Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
- run: flutter pub get
- run: dart format --output=none --set-exit-if-changed lib test example/lib example/integration_test examples/static_app/lib examples/riverpod_app/lib examples/bloc_getit_app/lib
- run: flutter analyze
- name: Validate Core contract and conformance fixtures
run: flutter test test/contract
- run: flutter test
- run: cd example && flutter pub get && flutter analyze
# Integration tests need a single device (-d). On ubuntu-latest both linux
Expand Down
16 changes: 16 additions & 0 deletions .pubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.dart_tool/
.github/
.idea/
.vscode/
*.iml
coverage/
build/
test/
examples/
plan
website/
**/.flutter-plugins
**/.flutter-plugins-dependencies
**/.dart_tool/
**/build/
**/coverage/
29 changes: 27 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
# 2.1.0 (2026-08-17)

## Added

- Published the versioned, language-neutral FlexTrack Core MVP specification
shared by the Flutter and Kotlin implementations.
- Added versioned JSON conformance fixtures, a Flutter runner and report, and
the Kotlin runner contract for cross-SDK behavior parity.

## Fixed

- Type-based routing now matches event subclasses and preserves the original
routing identity through `EnrichedEvent` transformers. Other routing
conditions still evaluate the transformed event.
- Replaced clock-modulo routing sampling with deterministic FNV-1a sampling
keyed by user id, session id, or event name. Essential events bypass
sampling, and published UTF-8 vectors keep future SDK implementations in
parity.
- Event instances now capture an immutable UUID v4 identifier and UTC
occurrence timestamp. Enrichment preserves both values.
- New clients now start with general and PII consent denied, matching the
documented privacy-safe default. Disabling consent checking on a routing
configuration now bypasses those checks as configured.
- `flexTrackVersion` now matches the package version declared in `pubspec.yaml`.

## 2.0.0

### Breaking changes
Expand Down Expand Up @@ -73,8 +98,8 @@ This release promotes the package to **1.0.0** and focuses on **injectable analy
### Documentation

* README: `FlexTrackClient`, `FlexTrackScope`, inspector section, table of contents.
* **`docs/flex-track-client.md`** — injectable client, Riverpod/Bloc, widget scope behavior.
* **`docs/assets/inspector.gif`** — demo of the inspector with the flagship app.
* **`doc/flex-track-client.md`** — injectable client, Riverpod/Bloc, widget scope behavior.
* **`doc/assets/inspector.gif`** — demo of the inspector with the flagship app.

---

Expand Down
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
![FlexTrack Banner](docs/assets/banner.png)
![FlexTrack Banner](doc/assets/banner.png)

# FlexTrack

Expand Down Expand Up @@ -37,7 +37,7 @@ Instead of spreading analytics policy throughout the app, define it once and app

## Visual Demo

![Inspector Demo](docs/assets/inspector.gif)
![Inspector Demo](doc/assets/inspector.gif)

## Quick Example

Expand Down Expand Up @@ -113,7 +113,7 @@ One call site, multiple tracker destinations, centralized policy.
```yaml
# pubspec.yaml
dependencies:
flex_track: ^1.0.0
flex_track: ^2.1.0
```

**Step 2 — implement your tracker** (the package ships no vendor SDKs; you write a thin adapter):
Expand Down Expand Up @@ -207,6 +207,10 @@ FlexTrack exists to make that architecture explicit, maintainable, and debuggabl
- `FlexTrackClient` for dependency injection patterns
- Widget wrappers for click, impression, mount, and route-view tracking

The normative behavior shared by the Flutter and Kotlin SDKs is defined in the
[FlexTrack Core MVP specification](doc/core-mvp-specification.md) and verified
with [shared conformance fixtures](doc/conformance.md).

---

## Examples
Expand Down Expand Up @@ -301,7 +305,7 @@ class CheckoutCubit extends Cubit<CheckoutState> {

For **strict** clean architecture, wrap `FlexTrackClient` behind your own `Analytics` interface in the domain module and implement the adapter in infrastructure.

More detail: [docs/flex-track-client.md](docs/flex-track-client.md).
More detail: [doc/flex-track-client.md](doc/flex-track-client.md).

---

Expand All @@ -315,6 +319,12 @@ This package does not bundle Firebase, Mixpanel, Amplitude, or any other analyti

Extend `BaseEvent` and implement the `name` and `properties` getters. Everything else is optional.

Every event receives an immutable UUID v4 `eventId` and UTC `timestamp` when
it is constructed. Those values remain unchanged through enrichment and
dispatch. For replay or restored offline events, pass the original metadata to
`super(eventId: storedId, timestamp: storedTimestamp)` from your event
constructor.

```dart
class PurchaseEvent extends BaseEvent {
final double amount;
Expand Down Expand Up @@ -474,6 +484,11 @@ FlexTrack.addTransformer((event) => EnrichedEvent(event, {

`EnrichedEvent` is a `BaseEvent` wrapper. It forwards all metadata from the original event (`category`, `containsPII`, `requiresConsent`, etc.) and overrides `properties` to merge the original properties with the extra ones. Extra properties win on key collision.

Type-based routes remain anchored to the original event through any number of
`EnrichedEvent` wrappers. A `route<PurchaseEvent>()` rule therefore continues
to match enriched purchases and subclasses of `PurchaseEvent`, while
property-based routes can still match properties added by transformers.

```dart
// Extra properties override originals on the same key.
EnrichedEvent(originalEvent, {'source': 'transformer'})
Expand Down Expand Up @@ -923,7 +938,17 @@ GDPRDefaults.applyStrict(routing, compliantTrackers: ['internal']);

## Sampling and performance

Sampling is applied per-rule. Each matching event independently has a random chance of being forwarded at the specified rate.
Sampling is applied per rule and is deterministic by default. FlexTrack hashes
the first non-empty value from `event.userId`, `event.sessionId`, and
`event.name` with FNV-1a over UTF-8 bytes. The resulting stable bucket is
compared with the rule's rate, so the same identity receives the same decision
across launches and SDK implementations. Essential events always bypass
sampling.

When no user or session identity is available, all events with the same name
share a decision. Supply a stable user or session id when you need a
representative user-level sample. The cross-platform vectors are published in
`test/fixtures/sampling_vectors.json`.

| Method | Rate |
|--------|------|
Expand Down Expand Up @@ -983,7 +1008,7 @@ FlexTrack Inspector (open in browser): http://127.0.0.1:7788

Open that address in a browser to inspect the live event list, tracker status, consent snapshot, and per-event JSON.

![FlexTrack Inspector dashboard with the flagship example app](docs/assets/inspector.gif)
![FlexTrack Inspector dashboard with the flagship example app](doc/assets/inspector.gif)

```dart
import 'package:flex_track/flex_track_inspector.dart';
Expand Down Expand Up @@ -1053,7 +1078,7 @@ await FlexTrack.setup([

**Global singleton** (existing pattern): use `setupFlexTrackForTesting()` and `FlexTrack.reset()` in `tearDown`.

**Injectable client** (no global): create a `FlexTrackClient` with a `MockTracker`, pass it into your class under test, and call `await client.dispose()` in `tearDown`. See [docs/flex-track-client.md](docs/flex-track-client.md).
**Injectable client** (no global): create a `FlexTrackClient` with a `MockTracker`, pass it into your class under test, and call `await client.dispose()` in `tearDown`. See [doc/flex-track-client.md](doc/flex-track-client.md).

```dart
import 'package:flutter_test/flutter_test.dart';
Expand Down
4 changes: 4 additions & 0 deletions docs/README.md → doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ Long-form documentation now lives in the **Docusaurus** site under [`website/doc

Historical topic filenames (`trackers.md`, `routing-and-rules.md`, etc.) have corresponding pages in `website/docs/guides/` (when the Docusaurus site is present).

- **[Core MVP specification](core-mvp-specification.md)** — normative,
language-neutral contract shared by Flutter and Kotlin.
- **[Cross-SDK conformance](conformance.md)** — shared fixtures, reports, and
the Kotlin runner contract.
- **[FlexTrackClient and DI](flex-track-client.md)** — injectable client, Riverpod and Bloc examples, tests without the global singleton.
File renamed without changes
File renamed without changes
File renamed without changes
65 changes: 65 additions & 0 deletions doc/conformance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Cross-SDK conformance

The files in
[`test/fixtures/conformance/`](https://github.com/alirezat66/flex_track/tree/main/test/fixtures/conformance)
are
the shared executable contract for Flutter and Kotlin Core MVP implementations.

## Version 1.0.0 files

- `core_mvp.schema.json` defines the fixture envelope.
- `core_mvp_cases.json` contains deterministic inputs and expected outputs.
- `flutter_report.json` is the machine-readable Flutter conformance report.
- `sampling_vectors.json` contains the complete Unicode FNV-1a vectors used by
both SDKs.

Fixture case IDs are stable within a fixture major version. Adding a
backward-compatible case increments the fixture minor version. Changing an
existing input or expected result increments the fixture major version.

## Kotlin runner contract

The Android repository MUST copy or consume the fixture files without rewriting
their values. Its runner MUST:

1. Reject an unsupported `specVersion` or fixture major version.
2. Validate the fixture envelope against `core_mvp.schema.json`.
3. Execute every case according to its `behavior` value.
4. Compare ordered arrays exactly; tracker ordering is observable.
5. Use UTF-8 and unsigned 32-bit FNV-1a for sampling cases.
6. Avoid wall-clock time, random identifiers, network calls, and Android device
state while evaluating fixtures.
7. Emit a JSON report with `specVersion`, `fixtureVersion`, `implementation`,
`total`, `passed`, `failed`, and ordered `caseIds`.
8. Exit unsuccessfully when schema validation or any case fails.

Example Kotlin report:

```json
{
"specVersion": "1.0.0",
"fixtureVersion": "1.0.0",
"implementation": "kotlin",
"total": 8,
"passed": 8,
"failed": 0,
"caseIds": ["routing.priority-overlap"]
}
```

The abbreviated `caseIds` above is illustrative; a real passing report MUST
contain every fixture ID in fixture order.

## Covered behavior

The MVP suite covers priority overlap, same-tier merging, fallback, missing
general consent, missing PII consent, Unicode sampling, enrichment identity and
property precedence, and the debug dispatch decision. Later capabilities such
as offline queues and retry are intentionally excluded until their contracts
are versioned.

Run the Flutter suite with:

```bash
flutter test test/contract
```
Loading
Loading