Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 112 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,144 @@
[![Pub Popularity](https://img.shields.io/pub/popularity/reelevant_analytics)](https://pub.dev/packages/reelevant_analytics/score)
[![Pub Publisher](https://img.shields.io/pub/publisher/reelevant_analytics)](https://pub.dev/publishers/reelevant.com/packages)

# Reelevant Analytics SDK for flutter (iOS and Android)
# Reelevant SDK for Flutter (iOS and Android)

This Flutter package could be used to send tracking events to Reelevant datasources.
Analytics tracking **and** real-time personalisation for Flutter apps, powered by Reelevant.

## Install

Run this command:

```
flutter pub add reelevant_analytics
```
See [pub.dev](https://pub.dev/packages/reelevant_analytics/install) for more informations.
See [pub.dev](https://pub.dev/packages/reelevant_analytics/install) for more information.

## How to use

You need to have a `datasourceId` and a `companyId` to be able to init the SDK and start sending events:
You need a `datasourceId` and a `companyId` to initialise the SDK:

```dart
final reelevantAnalytics = ReelevantAnalytics(companyId: '<company id>', datasourceId: '<datasource id>');
final rlvt = ReelevantAnalytics(companyId: '<company id>', datasourceId: '<datasource id>');
```

## Analytics

// Generate an event
var event = reelevantAnalytics.pageView(labels: {});
// Send it
reelevantAnalytics.send(event);
### Sending events

```dart
var event = rlvt.pageView(labels: {});
rlvt.send(event);
```

### Current URL

When a user is browsing a page you should call the `sdk.setCurrentURL` method if you want to be able to filter on it in Reelevant.
When a user is browsing a page you should call the `rlvt.setCurrentURL` method if you want to be able to filter on it in Reelevant.

### User infos
### User identity

To identify a user, you should call the `sdk.setUser('<user id>')` method which will store the user id in the device and send it to Reelevant.
To identify a user, call `rlvt.setUser('<user id>')` — the SDK stores the user ID on-device and sends it with every event and personalization call.

### Labels

Each event type allow you to pass additional infos via `labels` (`Map<String, String>`) on which you'll be able to filter in Reelevant.
Each event type allows you to pass additional info via `labels` (`Map<String, String>`) on which you'll be able to filter in Reelevant.

```dart
var event = rlvt.addCart(ids: ['my-product-id'], labels: {'lang': 'en_US'});
```

## Personalisation

The SDK can call the Reelevant runner to fetch personalised content for your app. Identity is automatically resolved from `setUser()` / device ID — no need to pass it manually.

### Configuration

Personalization parameters are optional (defaults work out of the box):

```dart
var event = reelevantAnalytics.addCart(ids: ['my-product-id'], labels: {'lang': 'en_US'});
final rlvt = ReelevantAnalytics(
companyId: '...',
datasourceId: '...',
// optional — defaults below
runnerUrl: 'https://reelevant.run',
runnerTimeout: Duration(seconds: 5),
fallback: FallbackStrategy.empty,
);
```

### Single workflow run

```dart
final result = await rlvt.run(RunOptions(
workflowId: 'wf-hero',
entrypoint: '43a490a0',
));

if (result.body is JsonRunContent) {
final data = (result.body as JsonRunContent).content;
renderCard(data);
} else if (result.body is HtmlRunContent) {
loadHtml((result.body as HtmlRunContent).content);
} else if (result.body is ImageRunContent) {
displayImage((result.body as ImageRunContent).content);
} else {
showDefault();
}
```

### Multiple workflows in parallel

```dart
final results = await rlvt.runAll([
RunOptions(workflowId: 'wf-hero', entrypoint: '43a490a0'),
RunOptions(workflowId: 'wf-sidebar', entrypoint: 'b7e21f3c'),
]);
```

### Click tracking

Every `RunResult` includes a `redirectionUrl` (for use as a link href) and a `trackClick()` method for fire-and-forget server-side tracking:

```dart
// Option 1: Use redirectionUrl as a link
launchUrl(Uri.parse(result.redirectionUrl));

// Option 2: Track the click programmatically
await result.trackClick();
```

### RunResult fields

| Field | Type | Description |
|-------|------|-------------|
| `status` | `int` | HTTP status code (0 for fallback) |
| `source` | `RunSource` | `runner` or `fallback` |
| `body` | `RunContent` | Typed content (`JsonRunContent`, `HtmlRunContent`, `ImageRunContent`, or `EmptyRunContent`) |
| `metadata` | `Map<String, dynamic>` | Metadata from the output node |
| `properties` | `Map<String, dynamic>` | Output properties |
| `runId` | `String?` | Workflow run ID for tracking |
| `executionPath` | `List<String>` | Branch IDs taken during execution |
| `redirectionUrl` | `String` | Pre-built click-through URL |

### Fallback strategies

```dart
// Default — returns an empty result on error
FallbackStrategy.empty

// Throws the underlying error
FallbackStrategy.error
```

### Run options

| Option | Type | Description |
|--------|------|-------------|
| `workflowId` | `String` | Workflow ID |
| `entrypoint` | `String` | Entrypoint shortId |
| `userId` | `String?` | Override identity (default: auto-resolved) |
| `params` | `Map<String, String>?` | URL parameters forwarded to runner |
| `locale` | `String?` | Locale for content resolution |
| `timeout` | `Duration?` | Per-call timeout override |

## Contribute

This project is a Flutter [plug-in package](https://flutter.dev/developing-packages/), a specialized package that includes platform-specific implementation code for Android and iOS.
Expand Down
88 changes: 87 additions & 1 deletion lib/reelevant_analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'reelevant_analytics_platform_interface.dart';
import 'reelevant_client.dart';

export 'reelevant_client.dart';

/// Event define an analytics events compatible with Reelevant Events datsources.
class Event {
Expand Down Expand Up @@ -81,11 +84,27 @@ class ReelevantAnalytics {
String? currentUrl, userAgent;
http.Client client = http.Client();

/// Runner endpoint URL for personalization (default: production runner).
String runnerUrl;

/// Global timeout for runner calls (default: 5s).
Duration runnerTimeout;

/// Fallback strategy when a runner call fails.
FallbackStrategy fallback;

/// Custom fallback handler (used when [fallback] is not `empty` or `error`).
FallbackHandler? fallbackHandler;

ReelevantAnalytics(
{required this.companyId,
required this.datasourceId,
this.endpoint = '',
this.retry = 60}) {
this.retry = 60,
this.runnerUrl = defaultRunnerUrl,
this.runnerTimeout = defaultTimeout,
this.fallback = FallbackStrategy.empty,
this.fallbackHandler}) {
if (endpoint == '') {
endpoint = 'https://collector.reelevant.com/collect/$datasourceId/rlvt';
}
Expand Down Expand Up @@ -235,6 +254,73 @@ class ReelevantAnalytics {
currentUrl = url;
}

// ---------------------------------------------------------------------------
// Personalization API
// ---------------------------------------------------------------------------

/// Execute a single workflow run.
/// Returns a typed [RunResult] with a discriminated `body`.
/// userId is auto-resolved from stored identity ([setUser] / tmpId) unless overridden in [options].
///
/// ### Example
///
/// ```dart
/// final result = await rlvt.run(RunOptions(
/// workflowId: 'wf-hero',
/// entrypoint: '43a490a0',
/// ));
/// if (result.body is JsonRunContent) {
/// renderCard((result.body as JsonRunContent).content);
/// }
/// ```
Future<RunResult> run(RunOptions options) async {
try {
final effectiveUserId = options.userId ?? await _resolveUserId();
return await executeRunnerCall(
options: options,
runnerUrl: runnerUrl,
timeout: runnerTimeout,
userId: effectiveUserId,
client: client,
);
} catch (e) {
return _handleRunError(options, e);
}
}

/// Execute multiple workflow runs in parallel.
/// Returns results in the same order as the input options.
Future<List<RunResult>> runAll(List<RunOptions> optionsList) async {
return Future.wait(optionsList.map((opts) => run(opts)));
}

Future<String> _resolveUserId() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('userId') ??
prefs.getString('tmpId') ??
_randomIdentifier();
}
Comment thread
vmarchaud marked this conversation as resolved.

Future<RunResult> _handleRunError(RunOptions options, Object error) async {
switch (fallback) {
case FallbackStrategy.error:
throw error;
case FallbackStrategy.empty:
if (fallbackHandler != null) {
return fallbackHandler!(options, error);
}
return RunResult(
status: 0,
source: RunSource.fallback,
body: EmptyRunContent(),
metadata: {},
properties: {},
executionPath: [],
redirectionUrl: '',
);
}
}

// Private methods

/// Generate a random identifier.
Expand Down
Loading
Loading