tacilytics 0.0.0-stage → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +253 -2
- package/cli.js +173 -0
- package/day-salt.js +55 -0
- package/package.json +56 -4
- package/public/app.js +788 -0
- package/public/assets/DISCLAIMER-simple-icons.md +51 -0
- package/public/assets/LICENSE-lucide.txt +43 -0
- package/public/assets/LICENSE-simple-icons.md +30 -0
- package/public/assets/cosmic-bg.webp +0 -0
- package/public/assets/flags/LICENSE-flag-icons.txt +21 -0
- package/public/assets/flags/ad.svg +150 -0
- package/public/assets/flags/ae.svg +6 -0
- package/public/assets/flags/af.svg +81 -0
- package/public/assets/flags/ag.svg +14 -0
- package/public/assets/flags/ai.svg +29 -0
- package/public/assets/flags/al.svg +5 -0
- package/public/assets/flags/am.svg +5 -0
- package/public/assets/flags/ao.svg +13 -0
- package/public/assets/flags/aq.svg +5 -0
- package/public/assets/flags/ar.svg +32 -0
- package/public/assets/flags/arab.svg +109 -0
- package/public/assets/flags/as.svg +72 -0
- package/public/assets/flags/asean.svg +13 -0
- package/public/assets/flags/at.svg +4 -0
- package/public/assets/flags/au.svg +8 -0
- package/public/assets/flags/aw.svg +186 -0
- package/public/assets/flags/ax.svg +18 -0
- package/public/assets/flags/az.svg +8 -0
- package/public/assets/flags/ba.svg +12 -0
- package/public/assets/flags/bb.svg +6 -0
- package/public/assets/flags/bd.svg +4 -0
- package/public/assets/flags/be.svg +7 -0
- package/public/assets/flags/bf.svg +7 -0
- package/public/assets/flags/bg.svg +5 -0
- package/public/assets/flags/bh.svg +4 -0
- package/public/assets/flags/bi.svg +15 -0
- package/public/assets/flags/bj.svg +14 -0
- package/public/assets/flags/bl.svg +5 -0
- package/public/assets/flags/bm.svg +97 -0
- package/public/assets/flags/bn.svg +36 -0
- package/public/assets/flags/bo.svg +673 -0
- package/public/assets/flags/bq.svg +5 -0
- package/public/assets/flags/br.svg +45 -0
- package/public/assets/flags/bs.svg +13 -0
- package/public/assets/flags/bt.svg +89 -0
- package/public/assets/flags/bv.svg +13 -0
- package/public/assets/flags/bw.svg +7 -0
- package/public/assets/flags/by.svg +18 -0
- package/public/assets/flags/bz.svg +145 -0
- package/public/assets/flags/ca.svg +4 -0
- package/public/assets/flags/cc.svg +19 -0
- package/public/assets/flags/cd.svg +5 -0
- package/public/assets/flags/cefta.svg +13 -0
- package/public/assets/flags/cf.svg +15 -0
- package/public/assets/flags/cg.svg +12 -0
- package/public/assets/flags/ch.svg +9 -0
- package/public/assets/flags/ci.svg +7 -0
- package/public/assets/flags/ck.svg +9 -0
- package/public/assets/flags/cl.svg +13 -0
- package/public/assets/flags/cm.svg +15 -0
- package/public/assets/flags/cn.svg +11 -0
- package/public/assets/flags/co.svg +7 -0
- package/public/assets/flags/cp.svg +7 -0
- package/public/assets/flags/cr.svg +7 -0
- package/public/assets/flags/cu.svg +13 -0
- package/public/assets/flags/cv.svg +13 -0
- package/public/assets/flags/cw.svg +14 -0
- package/public/assets/flags/cx.svg +15 -0
- package/public/assets/flags/cy.svg +6 -0
- package/public/assets/flags/cz.svg +5 -0
- package/public/assets/flags/de.svg +5 -0
- package/public/assets/flags/dg.svg +130 -0
- package/public/assets/flags/dj.svg +13 -0
- package/public/assets/flags/dk.svg +5 -0
- package/public/assets/flags/dm.svg +152 -0
- package/public/assets/flags/do.svg +121 -0
- package/public/assets/flags/dz.svg +5 -0
- package/public/assets/flags/eac.svg +48 -0
- package/public/assets/flags/ec.svg +138 -0
- package/public/assets/flags/ee.svg +5 -0
- package/public/assets/flags/eg.svg +38 -0
- package/public/assets/flags/eh.svg +16 -0
- package/public/assets/flags/er.svg +8 -0
- package/public/assets/flags/es-ct.svg +4 -0
- package/public/assets/flags/es-ga.svg +187 -0
- package/public/assets/flags/es-pv.svg +5 -0
- package/public/assets/flags/es.svg +544 -0
- package/public/assets/flags/et.svg +14 -0
- package/public/assets/flags/eu.svg +28 -0
- package/public/assets/flags/fi.svg +5 -0
- package/public/assets/flags/fj.svg +120 -0
- package/public/assets/flags/fk.svg +90 -0
- package/public/assets/flags/fm.svg +11 -0
- package/public/assets/flags/fo.svg +12 -0
- package/public/assets/flags/fr.svg +5 -0
- package/public/assets/flags/ga.svg +7 -0
- package/public/assets/flags/gb-eng.svg +5 -0
- package/public/assets/flags/gb-nir.svg +132 -0
- package/public/assets/flags/gb-sct.svg +4 -0
- package/public/assets/flags/gb-wls.svg +9 -0
- package/public/assets/flags/gb.svg +7 -0
- package/public/assets/flags/gd.svg +27 -0
- package/public/assets/flags/ge.svg +6 -0
- package/public/assets/flags/gf.svg +5 -0
- package/public/assets/flags/gg.svg +9 -0
- package/public/assets/flags/gh.svg +6 -0
- package/public/assets/flags/gi.svg +32 -0
- package/public/assets/flags/gl.svg +4 -0
- package/public/assets/flags/gm.svg +14 -0
- package/public/assets/flags/gn.svg +7 -0
- package/public/assets/flags/gp.svg +5 -0
- package/public/assets/flags/gq.svg +23 -0
- package/public/assets/flags/gr.svg +16 -0
- package/public/assets/flags/gs.svg +133 -0
- package/public/assets/flags/gt.svg +204 -0
- package/public/assets/flags/gu.svg +19 -0
- package/public/assets/flags/gw.svg +13 -0
- package/public/assets/flags/gy.svg +9 -0
- package/public/assets/flags/hk.svg +8 -0
- package/public/assets/flags/hm.svg +8 -0
- package/public/assets/flags/hn.svg +18 -0
- package/public/assets/flags/hr.svg +58 -0
- package/public/assets/flags/ht.svg +116 -0
- package/public/assets/flags/hu.svg +7 -0
- package/public/assets/flags/ic.svg +7 -0
- package/public/assets/flags/id.svg +4 -0
- package/public/assets/flags/ie.svg +7 -0
- package/public/assets/flags/il.svg +14 -0
- package/public/assets/flags/im.svg +36 -0
- package/public/assets/flags/in.svg +25 -0
- package/public/assets/flags/io.svg +130 -0
- package/public/assets/flags/iq.svg +10 -0
- package/public/assets/flags/ir.svg +219 -0
- package/public/assets/flags/is.svg +12 -0
- package/public/assets/flags/it.svg +7 -0
- package/public/assets/flags/je.svg +62 -0
- package/public/assets/flags/jm.svg +8 -0
- package/public/assets/flags/jo.svg +16 -0
- package/public/assets/flags/jp.svg +11 -0
- package/public/assets/flags/ke.svg +23 -0
- package/public/assets/flags/kg.svg +4 -0
- package/public/assets/flags/kh.svg +61 -0
- package/public/assets/flags/ki.svg +36 -0
- package/public/assets/flags/km.svg +16 -0
- package/public/assets/flags/kn.svg +14 -0
- package/public/assets/flags/kp.svg +15 -0
- package/public/assets/flags/kr.svg +24 -0
- package/public/assets/flags/kw.svg +13 -0
- package/public/assets/flags/ky.svg +103 -0
- package/public/assets/flags/kz.svg +36 -0
- package/public/assets/flags/la.svg +12 -0
- package/public/assets/flags/lb.svg +15 -0
- package/public/assets/flags/lc.svg +8 -0
- package/public/assets/flags/li.svg +43 -0
- package/public/assets/flags/lk.svg +22 -0
- package/public/assets/flags/lr.svg +14 -0
- package/public/assets/flags/ls.svg +8 -0
- package/public/assets/flags/lt.svg +7 -0
- package/public/assets/flags/lu.svg +5 -0
- package/public/assets/flags/lv.svg +6 -0
- package/public/assets/flags/ly.svg +13 -0
- package/public/assets/flags/ma.svg +4 -0
- package/public/assets/flags/mc.svg +6 -0
- package/public/assets/flags/md.svg +70 -0
- package/public/assets/flags/me.svg +116 -0
- package/public/assets/flags/mf.svg +5 -0
- package/public/assets/flags/mg.svg +7 -0
- package/public/assets/flags/mh.svg +7 -0
- package/public/assets/flags/mk.svg +5 -0
- package/public/assets/flags/ml.svg +7 -0
- package/public/assets/flags/mm.svg +12 -0
- package/public/assets/flags/mn.svg +14 -0
- package/public/assets/flags/mo.svg +9 -0
- package/public/assets/flags/mp.svg +86 -0
- package/public/assets/flags/mq.svg +5 -0
- package/public/assets/flags/mr.svg +6 -0
- package/public/assets/flags/ms.svg +29 -0
- package/public/assets/flags/mt.svg +58 -0
- package/public/assets/flags/mu.svg +8 -0
- package/public/assets/flags/mv.svg +6 -0
- package/public/assets/flags/mw.svg +10 -0
- package/public/assets/flags/mx.svg +382 -0
- package/public/assets/flags/my.svg +26 -0
- package/public/assets/flags/mz.svg +21 -0
- package/public/assets/flags/na.svg +16 -0
- package/public/assets/flags/nc.svg +13 -0
- package/public/assets/flags/ne.svg +6 -0
- package/public/assets/flags/nf.svg +9 -0
- package/public/assets/flags/ng.svg +6 -0
- package/public/assets/flags/ni.svg +129 -0
- package/public/assets/flags/nl.svg +5 -0
- package/public/assets/flags/no.svg +7 -0
- package/public/assets/flags/np.svg +13 -0
- package/public/assets/flags/nr.svg +12 -0
- package/public/assets/flags/nu.svg +10 -0
- package/public/assets/flags/nz.svg +36 -0
- package/public/assets/flags/om.svg +115 -0
- package/public/assets/flags/pa.svg +14 -0
- package/public/assets/flags/pc.svg +33 -0
- package/public/assets/flags/pe.svg +4 -0
- package/public/assets/flags/pf.svg +19 -0
- package/public/assets/flags/pg.svg +9 -0
- package/public/assets/flags/ph.svg +6 -0
- package/public/assets/flags/pk.svg +15 -0
- package/public/assets/flags/pl.svg +6 -0
- package/public/assets/flags/pm.svg +5 -0
- package/public/assets/flags/pn.svg +53 -0
- package/public/assets/flags/pr.svg +13 -0
- package/public/assets/flags/ps.svg +6 -0
- package/public/assets/flags/pt.svg +57 -0
- package/public/assets/flags/pw.svg +11 -0
- package/public/assets/flags/py.svg +157 -0
- package/public/assets/flags/qa.svg +4 -0
- package/public/assets/flags/re.svg +5 -0
- package/public/assets/flags/ro.svg +7 -0
- package/public/assets/flags/rs.svg +292 -0
- package/public/assets/flags/ru.svg +5 -0
- package/public/assets/flags/rw.svg +13 -0
- package/public/assets/flags/sa.svg +25 -0
- package/public/assets/flags/sb.svg +13 -0
- package/public/assets/flags/sc.svg +7 -0
- package/public/assets/flags/sd.svg +13 -0
- package/public/assets/flags/se.svg +4 -0
- package/public/assets/flags/sg.svg +13 -0
- package/public/assets/flags/sh-ac.svg +689 -0
- package/public/assets/flags/sh-hl.svg +164 -0
- package/public/assets/flags/sh-ta.svg +76 -0
- package/public/assets/flags/sh.svg +7 -0
- package/public/assets/flags/si.svg +18 -0
- package/public/assets/flags/sj.svg +7 -0
- package/public/assets/flags/sk.svg +9 -0
- package/public/assets/flags/sl.svg +7 -0
- package/public/assets/flags/sm.svg +75 -0
- package/public/assets/flags/sn.svg +8 -0
- package/public/assets/flags/so.svg +11 -0
- package/public/assets/flags/sr.svg +6 -0
- package/public/assets/flags/ss.svg +8 -0
- package/public/assets/flags/st.svg +16 -0
- package/public/assets/flags/sv.svg +593 -0
- package/public/assets/flags/sx.svg +56 -0
- package/public/assets/flags/sy.svg +6 -0
- package/public/assets/flags/sz.svg +34 -0
- package/public/assets/flags/tc.svg +50 -0
- package/public/assets/flags/td.svg +7 -0
- package/public/assets/flags/tf.svg +15 -0
- package/public/assets/flags/tg.svg +14 -0
- package/public/assets/flags/th.svg +7 -0
- package/public/assets/flags/tj.svg +22 -0
- package/public/assets/flags/tk.svg +5 -0
- package/public/assets/flags/tl.svg +13 -0
- package/public/assets/flags/tm.svg +204 -0
- package/public/assets/flags/tn.svg +4 -0
- package/public/assets/flags/to.svg +10 -0
- package/public/assets/flags/tr.svg +8 -0
- package/public/assets/flags/tt.svg +5 -0
- package/public/assets/flags/tv.svg +9 -0
- package/public/assets/flags/tw.svg +34 -0
- package/public/assets/flags/tz.svg +13 -0
- package/public/assets/flags/ua.svg +6 -0
- package/public/assets/flags/ug.svg +30 -0
- package/public/assets/flags/um.svg +9 -0
- package/public/assets/flags/un.svg +16 -0
- package/public/assets/flags/us.svg +9 -0
- package/public/assets/flags/uy.svg +28 -0
- package/public/assets/flags/uz.svg +30 -0
- package/public/assets/flags/va.svg +190 -0
- package/public/assets/flags/vc.svg +8 -0
- package/public/assets/flags/ve.svg +26 -0
- package/public/assets/flags/vg.svg +59 -0
- package/public/assets/flags/vi.svg +28 -0
- package/public/assets/flags/vn.svg +11 -0
- package/public/assets/flags/vu.svg +21 -0
- package/public/assets/flags/wf.svg +5 -0
- package/public/assets/flags/ws.svg +7 -0
- package/public/assets/flags/xk.svg +5 -0
- package/public/assets/flags/xx.svg +4 -0
- package/public/assets/flags/ye.svg +7 -0
- package/public/assets/flags/yt.svg +5 -0
- package/public/assets/flags/za.svg +17 -0
- package/public/assets/flags/zm.svg +27 -0
- package/public/assets/flags/zw.svg +21 -0
- package/public/assets/fonts/Inter-OFL.txt +93 -0
- package/public/assets/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/public/assets/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/public/assets/icons.svg +200 -0
- package/public/assets/nyx-logo-256.png +0 -0
- package/public/assets/tacilytics-mark.svg +16 -0
- package/public/index.html +239 -0
- package/public/style.css +415 -0
- package/public/tacilytics.js +184 -0
- package/sdk/flutter/CHANGELOG.md +6 -0
- package/sdk/flutter/LICENSE +21 -0
- package/sdk/flutter/README.md +41 -0
- package/sdk/flutter/dart_test.yaml +3 -0
- package/sdk/flutter/lib/nyx_analytics.dart +13 -0
- package/sdk/flutter/lib/src/beacon.dart +87 -0
- package/sdk/flutter/lib/src/client.dart +177 -0
- package/sdk/flutter/lib/tacilytics.dart +13 -0
- package/sdk/flutter/pubspec.lock +221 -0
- package/sdk/flutter/pubspec.yaml +18 -0
- package/sdk/flutter/test/alias_test.dart +13 -0
- package/sdk/flutter/test/beacon_test.dart +46 -0
- package/sdk/flutter/test/e2e_test.dart +18 -0
- package/server.js +896 -0
- package/tz-country.js +32 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# tacilytics (Flutter)
|
|
2
|
+
|
|
3
|
+
Privacy-first, cookieless app analytics for [Tacilytics](https://github.com/Open-Cosmic-Software/tacilytics).
|
|
4
|
+
Screen views, goals, in-app search and revenue land in the same dashboard as your website, filterable by platform.
|
|
5
|
+
|
|
6
|
+
- **Off by default.** Nothing is sent until `setEnabled(true)`, so you can wire it to your consent.
|
|
7
|
+
- **No device id, no advertising id, no cookies.** One random id per app launch, kept in memory only.
|
|
8
|
+
- **Fire-and-forget.** Never throws into your app, never blocks the UI.
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
dependencies:
|
|
12
|
+
tacilytics:
|
|
13
|
+
git:
|
|
14
|
+
url: https://github.com/Open-Cosmic-Software/tacilytics
|
|
15
|
+
path: sdk/flutter
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```dart
|
|
19
|
+
void main() {
|
|
20
|
+
Tacilytics.init(endpoint: 'https://analytics.example.com', domain: 'example.com', appVersion: '1.4.0');
|
|
21
|
+
runApp(MaterialApp(navigatorObservers: [TacilyticsRouteObserver()], home: const Home()));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// after your consent dialog
|
|
25
|
+
Tacilytics.instance.setEnabled(accepted);
|
|
26
|
+
|
|
27
|
+
Tacilytics.instance.screen('/product/42'); // manual screen view
|
|
28
|
+
Tacilytics.instance.event('Signup', {'plan': 'pro'}); // goal with properties
|
|
29
|
+
Tacilytics.instance.search('vinegar', 6); // 0 results show up under "No results"
|
|
30
|
+
Tacilytics.instance.purchase(value: 49.90, items: 2); // revenue — never pass customer data
|
|
31
|
+
Tacilytics.instance.setCampaign(source: 'newsletter'); // e.g. from a deep link
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use the same `domain` as your website to see web and app side by side. On Flutter web the client stays silent; use the `tacilytics.js` tag there.
|
|
35
|
+
|
|
36
|
+
Tests: `flutter test` (unit) · `TACILYTICS_E2E=http://localhost:3920 flutter test --tags e2e --run-skipped` (against a running server).
|
|
37
|
+
|
|
38
|
+
MIT licensed.
|
|
39
|
+
|
|
40
|
+
## Upgrading from `nyx_analytics`
|
|
41
|
+
Rename the dependency to `tacilytics` and import `package:tacilytics/tacilytics.dart`. If you can't change your code yet, import `package:tacilytics/nyx_analytics.dart`: `NyxAnalytics`, `NyxBeacon` and `NyxRouteObserver` still work as deprecated aliases.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// Deprecated entry point from before the rename to Tacilytics.
|
|
2
|
+
/// Import `package:tacilytics/tacilytics.dart` instead; these aliases will be removed in a later major version.
|
|
3
|
+
library nyx_analytics;
|
|
4
|
+
|
|
5
|
+
import 'tacilytics.dart';
|
|
6
|
+
export 'tacilytics.dart';
|
|
7
|
+
|
|
8
|
+
@Deprecated('Use Tacilytics')
|
|
9
|
+
typedef NyxAnalytics = Tacilytics;
|
|
10
|
+
@Deprecated('Use TacilyticsBeacon')
|
|
11
|
+
typedef NyxBeacon = TacilyticsBeacon;
|
|
12
|
+
@Deprecated('Use TacilyticsRouteObserver')
|
|
13
|
+
typedef NyxRouteObserver = TacilyticsRouteObserver;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import 'dart:math';
|
|
2
|
+
|
|
3
|
+
/// Builds the JSON body for `POST /api/collect`. Pure functions, no I/O — fully unit-testable.
|
|
4
|
+
///
|
|
5
|
+
/// Wire format (shared with the web tracker, plus the app-only fields):
|
|
6
|
+
/// d site domain p path / screen name
|
|
7
|
+
/// n event name (optional) pr event properties (flat, scalars only)
|
|
8
|
+
/// pf platform: android|ios|macos|windows|linux
|
|
9
|
+
/// av app version dv mobile|tablet|desktop
|
|
10
|
+
/// l language (de-AT) cc 2-letter region of the device locale
|
|
11
|
+
/// sid random per-launch id (never persisted, never a device id)
|
|
12
|
+
/// s / um / uc utm source / medium / campaign (e.g. from a deep link)
|
|
13
|
+
class TacilyticsBeacon {
|
|
14
|
+
TacilyticsBeacon._();
|
|
15
|
+
|
|
16
|
+
static const _sidChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
17
|
+
|
|
18
|
+
/// A fresh random session id: 24 chars, cryptographically random. Lives only in memory.
|
|
19
|
+
static String newSessionId([Random? rnd]) {
|
|
20
|
+
final r = rnd ?? Random.secure();
|
|
21
|
+
return List.generate(24, (_) => _sidChars[r.nextInt(_sidChars.length)]).join();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Normalises a screen name into a path: `ProductPage` stays, `product/42` → `/product/42`.
|
|
25
|
+
static String path(String screen) {
|
|
26
|
+
var p = screen.trim();
|
|
27
|
+
if (p.isEmpty) return '/';
|
|
28
|
+
if (!p.startsWith('/')) p = '/$p';
|
|
29
|
+
if (p.length > 1 && p.endsWith('/')) p = p.substring(0, p.length - 1);
|
|
30
|
+
return p.length > 512 ? p.substring(0, 512) : p;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Properties are flat key → scalar. Nested values and nulls are dropped, max 12 keys,
|
|
34
|
+
/// everything stringified — the same rules the server enforces, so nothing is silently lost twice.
|
|
35
|
+
static Map<String, String>? props(Map<String, Object?>? raw) {
|
|
36
|
+
if (raw == null || raw.isEmpty) return null;
|
|
37
|
+
final out = <String, String>{};
|
|
38
|
+
for (final e in raw.entries) {
|
|
39
|
+
if (out.length >= 12) break;
|
|
40
|
+
final v = e.value;
|
|
41
|
+
if (v == null || v is Map || v is Iterable) continue;
|
|
42
|
+
final k = e.key.trim();
|
|
43
|
+
if (k.isEmpty) continue;
|
|
44
|
+
final s = v is double ? _num(v) : v.toString();
|
|
45
|
+
out[k.length > 40 ? k.substring(0, 40) : k] = s.length > 80 ? s.substring(0, 80) : s;
|
|
46
|
+
}
|
|
47
|
+
return out.isEmpty ? null : out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static String _num(double v) => v == v.roundToDouble() ? v.toStringAsFixed(0) : v.toStringAsFixed(2);
|
|
51
|
+
|
|
52
|
+
static Map<String, Object> build({
|
|
53
|
+
required String domain,
|
|
54
|
+
required String platform,
|
|
55
|
+
required String sessionId,
|
|
56
|
+
required String screen,
|
|
57
|
+
String? name,
|
|
58
|
+
Map<String, Object?>? properties,
|
|
59
|
+
String? appVersion,
|
|
60
|
+
String? device,
|
|
61
|
+
String? language,
|
|
62
|
+
String? region,
|
|
63
|
+
String? utmSource,
|
|
64
|
+
String? utmMedium,
|
|
65
|
+
String? utmCampaign,
|
|
66
|
+
}) {
|
|
67
|
+
final body = <String, Object>{
|
|
68
|
+
'd': domain,
|
|
69
|
+
'p': path(screen),
|
|
70
|
+
'pf': platform,
|
|
71
|
+
'sid': sessionId,
|
|
72
|
+
};
|
|
73
|
+
if (name != null && name.trim().isNotEmpty) {
|
|
74
|
+
body['n'] = name.trim();
|
|
75
|
+
final pr = props(properties);
|
|
76
|
+
if (pr != null) body['pr'] = pr;
|
|
77
|
+
}
|
|
78
|
+
if (appVersion != null && appVersion.isNotEmpty) body['av'] = appVersion;
|
|
79
|
+
if (device != null) body['dv'] = device;
|
|
80
|
+
if (language != null && language.isNotEmpty) body['l'] = language;
|
|
81
|
+
if (region != null && RegExp(r'^[A-Za-z]{2}$').hasMatch(region)) body['cc'] = region.toUpperCase();
|
|
82
|
+
if (utmSource != null && utmSource.isNotEmpty) body['s'] = utmSource;
|
|
83
|
+
if (utmMedium != null && utmMedium.isNotEmpty) body['um'] = utmMedium;
|
|
84
|
+
if (utmCampaign != null && utmCampaign.isNotEmpty) body['uc'] = utmCampaign;
|
|
85
|
+
return body;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:convert';
|
|
3
|
+
|
|
4
|
+
import 'package:flutter/foundation.dart';
|
|
5
|
+
import 'package:flutter/widgets.dart';
|
|
6
|
+
import 'package:http/http.dart' as http;
|
|
7
|
+
|
|
8
|
+
import 'beacon.dart';
|
|
9
|
+
|
|
10
|
+
/// The client. One instance per app; call [Tacilytics.init] once at start-up.
|
|
11
|
+
///
|
|
12
|
+
/// Privacy by design:
|
|
13
|
+
/// * **Off until you call [setEnabled]`(true)`** — wire it to your consent banner.
|
|
14
|
+
/// * No device id, no advertising id, no cookies. A random session id is drawn per app
|
|
15
|
+
/// launch, kept in memory only, and the server hashes it with a daily salt.
|
|
16
|
+
/// * Region comes from the device locale, never from IP geolocation.
|
|
17
|
+
/// * Fire-and-forget: tracking never throws into your app and never blocks the UI.
|
|
18
|
+
class Tacilytics {
|
|
19
|
+
Tacilytics._(this.endpoint, this.domain, this.appVersion, this._client, this._platform);
|
|
20
|
+
|
|
21
|
+
static Tacilytics? _instance;
|
|
22
|
+
|
|
23
|
+
/// The configured client. Throws if [init] was not called.
|
|
24
|
+
static Tacilytics get instance {
|
|
25
|
+
final i = _instance;
|
|
26
|
+
if (i == null) throw StateError('Tacilytics.init() must be called first');
|
|
27
|
+
return i;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Configure once. [endpoint] is your Tacilytics server URL, [domain] the site as registered there
|
|
31
|
+
/// (use the same one as your website to see web + app side by side, filterable by platform).
|
|
32
|
+
static Tacilytics init({
|
|
33
|
+
required String endpoint,
|
|
34
|
+
required String domain,
|
|
35
|
+
String? appVersion,
|
|
36
|
+
bool enabled = false,
|
|
37
|
+
http.Client? client,
|
|
38
|
+
String? platformOverride,
|
|
39
|
+
}) {
|
|
40
|
+
final base = endpoint.endsWith('/') ? endpoint.substring(0, endpoint.length - 1) : endpoint;
|
|
41
|
+
final i = Tacilytics._(base, domain, appVersion, client ?? http.Client(), platformOverride ?? _detectPlatform());
|
|
42
|
+
i._enabled = enabled;
|
|
43
|
+
_instance = i;
|
|
44
|
+
return i;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
final String endpoint;
|
|
48
|
+
final String domain;
|
|
49
|
+
final String? appVersion;
|
|
50
|
+
final http.Client _client;
|
|
51
|
+
final String? _platform;
|
|
52
|
+
final String _sid = TacilyticsBeacon.newSessionId();
|
|
53
|
+
bool _enabled = false;
|
|
54
|
+
String? _lastScreen;
|
|
55
|
+
String? _utmSource, _utmMedium, _utmCampaign;
|
|
56
|
+
|
|
57
|
+
bool get enabled => _enabled;
|
|
58
|
+
|
|
59
|
+
/// Turn tracking on/off, e.g. from your consent dialog. Off = nothing leaves the device.
|
|
60
|
+
void setEnabled(bool on) => _enabled = on;
|
|
61
|
+
|
|
62
|
+
/// Attribute this launch to a campaign (e.g. read from a deep link's utm_* parameters).
|
|
63
|
+
void setCampaign({String? source, String? medium, String? campaign}) {
|
|
64
|
+
_utmSource = source;
|
|
65
|
+
_utmMedium = medium;
|
|
66
|
+
_utmCampaign = campaign;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// A screen view — the app equivalent of a pageview.
|
|
70
|
+
Future<void> screen(String name) {
|
|
71
|
+
_lastScreen = TacilyticsBeacon.path(name);
|
|
72
|
+
return _send(screen: name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// A custom event / goal, attached to the current screen.
|
|
76
|
+
Future<void> event(String name, [Map<String, Object?>? properties]) =>
|
|
77
|
+
_send(screen: _lastScreen ?? '/', name: name, properties: properties);
|
|
78
|
+
|
|
79
|
+
/// A completed order. Shows up in the revenue panels (by source and by platform).
|
|
80
|
+
/// Never pass customer data or order numbers.
|
|
81
|
+
Future<void> purchase({required double value, String currency = 'EUR', double? net, int? items}) =>
|
|
82
|
+
event('Purchase', {
|
|
83
|
+
'value': value,
|
|
84
|
+
'currency': currency,
|
|
85
|
+
if (net != null) 'net': net,
|
|
86
|
+
if (items != null) 'items': items,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/// An in-app search with its result count (0 shows up under "No results").
|
|
90
|
+
Future<void> search(String query, int results) => event('Search', {'q': query, 'results': results});
|
|
91
|
+
|
|
92
|
+
Future<void> _send({required String screen, String? name, Map<String, Object?>? properties}) async {
|
|
93
|
+
final pf = _platform;
|
|
94
|
+
if (!_enabled || pf == null) return; // web builds: use the tacilytics.js tracker instead
|
|
95
|
+
final locale = PlatformDispatcher.instance.locale;
|
|
96
|
+
final body = TacilyticsBeacon.build(
|
|
97
|
+
domain: domain,
|
|
98
|
+
platform: pf,
|
|
99
|
+
sessionId: _sid,
|
|
100
|
+
screen: screen,
|
|
101
|
+
name: name,
|
|
102
|
+
properties: properties,
|
|
103
|
+
appVersion: appVersion,
|
|
104
|
+
device: _deviceClass(),
|
|
105
|
+
language: locale.toLanguageTag(),
|
|
106
|
+
region: locale.countryCode,
|
|
107
|
+
utmSource: _utmSource,
|
|
108
|
+
utmMedium: _utmMedium,
|
|
109
|
+
utmCampaign: _utmCampaign,
|
|
110
|
+
);
|
|
111
|
+
try {
|
|
112
|
+
await _client
|
|
113
|
+
.post(
|
|
114
|
+
Uri.parse('$endpoint/api/collect'),
|
|
115
|
+
headers: {'Content-Type': 'application/json', 'User-Agent': 'Tacilytics-Flutter/0.2 ($pf)'},
|
|
116
|
+
body: jsonEncode(body),
|
|
117
|
+
)
|
|
118
|
+
.timeout(const Duration(seconds: 10));
|
|
119
|
+
} catch (_) {
|
|
120
|
+
// analytics must never break the app — a lost beacon is acceptable
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static String? _detectPlatform() {
|
|
125
|
+
if (kIsWeb) return null;
|
|
126
|
+
switch (defaultTargetPlatform) {
|
|
127
|
+
case TargetPlatform.android:
|
|
128
|
+
return 'android';
|
|
129
|
+
case TargetPlatform.iOS:
|
|
130
|
+
return 'ios';
|
|
131
|
+
case TargetPlatform.macOS:
|
|
132
|
+
return 'macos';
|
|
133
|
+
case TargetPlatform.windows:
|
|
134
|
+
return 'windows';
|
|
135
|
+
case TargetPlatform.linux:
|
|
136
|
+
return 'linux';
|
|
137
|
+
default:
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Phone vs. tablet from the logical screen size (shortest side ≥ 600 dp = tablet).
|
|
143
|
+
static String _deviceClass() {
|
|
144
|
+
final views = PlatformDispatcher.instance.views;
|
|
145
|
+
if (views.isEmpty) return 'mobile';
|
|
146
|
+
final v = views.first;
|
|
147
|
+
final size = v.physicalSize / v.devicePixelRatio;
|
|
148
|
+
final pf = defaultTargetPlatform;
|
|
149
|
+
if (pf == TargetPlatform.macOS || pf == TargetPlatform.windows || pf == TargetPlatform.linux) return 'desktop';
|
|
150
|
+
return size.shortestSide >= 600 ? 'tablet' : 'mobile';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Optional: track screen views automatically —
|
|
155
|
+
/// `MaterialApp(navigatorObservers: [TacilyticsRouteObserver()])`.
|
|
156
|
+
/// Uses the route's `settings.name`; unnamed routes (dialogs, sheets) are skipped.
|
|
157
|
+
class TacilyticsRouteObserver extends NavigatorObserver {
|
|
158
|
+
TacilyticsRouteObserver({this.nameOf});
|
|
159
|
+
|
|
160
|
+
/// Map a route to a screen name, e.g. to strip ids. Return null to skip the route.
|
|
161
|
+
final String? Function(Route<dynamic> route)? nameOf;
|
|
162
|
+
|
|
163
|
+
void _track(Route<dynamic>? route) {
|
|
164
|
+
if (route == null || Tacilytics._instance == null) return;
|
|
165
|
+
final name = nameOf != null ? nameOf!(route) : route.settings.name;
|
|
166
|
+
if (name != null && name.isNotEmpty) Tacilytics.instance.screen(name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@override
|
|
170
|
+
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => _track(route);
|
|
171
|
+
|
|
172
|
+
@override
|
|
173
|
+
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => _track(newRoute);
|
|
174
|
+
|
|
175
|
+
@override
|
|
176
|
+
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => _track(previousRoute);
|
|
177
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// Tacilytics for Flutter — privacy-first, cookieless app analytics.
|
|
2
|
+
/// (tacit: we count, we don't tell.)
|
|
3
|
+
///
|
|
4
|
+
/// ```dart
|
|
5
|
+
/// Tacilytics.init(endpoint: 'https://analytics.example.com', domain: 'example.com');
|
|
6
|
+
/// Tacilytics.instance.setEnabled(userConsented); // nothing is sent before this
|
|
7
|
+
/// Tacilytics.instance.screen('/product/42');
|
|
8
|
+
/// Tacilytics.instance.purchase(value: 49.90, currency: 'EUR');
|
|
9
|
+
/// ```
|
|
10
|
+
library tacilytics;
|
|
11
|
+
|
|
12
|
+
export 'src/client.dart';
|
|
13
|
+
export 'src/beacon.dart';
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Generated by pub
|
|
2
|
+
# See https://dart.dev/tools/pub/glossary#lockfile
|
|
3
|
+
packages:
|
|
4
|
+
async:
|
|
5
|
+
dependency: transitive
|
|
6
|
+
description:
|
|
7
|
+
name: async
|
|
8
|
+
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
|
9
|
+
url: "https://pub.dev"
|
|
10
|
+
source: hosted
|
|
11
|
+
version: "2.11.0"
|
|
12
|
+
boolean_selector:
|
|
13
|
+
dependency: transitive
|
|
14
|
+
description:
|
|
15
|
+
name: boolean_selector
|
|
16
|
+
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
|
17
|
+
url: "https://pub.dev"
|
|
18
|
+
source: hosted
|
|
19
|
+
version: "2.1.1"
|
|
20
|
+
characters:
|
|
21
|
+
dependency: transitive
|
|
22
|
+
description:
|
|
23
|
+
name: characters
|
|
24
|
+
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
|
25
|
+
url: "https://pub.dev"
|
|
26
|
+
source: hosted
|
|
27
|
+
version: "1.3.0"
|
|
28
|
+
clock:
|
|
29
|
+
dependency: transitive
|
|
30
|
+
description:
|
|
31
|
+
name: clock
|
|
32
|
+
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
|
33
|
+
url: "https://pub.dev"
|
|
34
|
+
source: hosted
|
|
35
|
+
version: "1.1.1"
|
|
36
|
+
collection:
|
|
37
|
+
dependency: transitive
|
|
38
|
+
description:
|
|
39
|
+
name: collection
|
|
40
|
+
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
|
41
|
+
url: "https://pub.dev"
|
|
42
|
+
source: hosted
|
|
43
|
+
version: "1.19.0"
|
|
44
|
+
fake_async:
|
|
45
|
+
dependency: transitive
|
|
46
|
+
description:
|
|
47
|
+
name: fake_async
|
|
48
|
+
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
|
49
|
+
url: "https://pub.dev"
|
|
50
|
+
source: hosted
|
|
51
|
+
version: "1.3.1"
|
|
52
|
+
flutter:
|
|
53
|
+
dependency: "direct main"
|
|
54
|
+
description: flutter
|
|
55
|
+
source: sdk
|
|
56
|
+
version: "0.0.0"
|
|
57
|
+
flutter_test:
|
|
58
|
+
dependency: "direct dev"
|
|
59
|
+
description: flutter
|
|
60
|
+
source: sdk
|
|
61
|
+
version: "0.0.0"
|
|
62
|
+
http:
|
|
63
|
+
dependency: "direct main"
|
|
64
|
+
description:
|
|
65
|
+
name: http
|
|
66
|
+
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
|
67
|
+
url: "https://pub.dev"
|
|
68
|
+
source: hosted
|
|
69
|
+
version: "1.6.0"
|
|
70
|
+
http_parser:
|
|
71
|
+
dependency: transitive
|
|
72
|
+
description:
|
|
73
|
+
name: http_parser
|
|
74
|
+
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
|
75
|
+
url: "https://pub.dev"
|
|
76
|
+
source: hosted
|
|
77
|
+
version: "4.1.2"
|
|
78
|
+
leak_tracker:
|
|
79
|
+
dependency: transitive
|
|
80
|
+
description:
|
|
81
|
+
name: leak_tracker
|
|
82
|
+
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
|
83
|
+
url: "https://pub.dev"
|
|
84
|
+
source: hosted
|
|
85
|
+
version: "10.0.7"
|
|
86
|
+
leak_tracker_flutter_testing:
|
|
87
|
+
dependency: transitive
|
|
88
|
+
description:
|
|
89
|
+
name: leak_tracker_flutter_testing
|
|
90
|
+
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
|
91
|
+
url: "https://pub.dev"
|
|
92
|
+
source: hosted
|
|
93
|
+
version: "3.0.8"
|
|
94
|
+
leak_tracker_testing:
|
|
95
|
+
dependency: transitive
|
|
96
|
+
description:
|
|
97
|
+
name: leak_tracker_testing
|
|
98
|
+
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
|
99
|
+
url: "https://pub.dev"
|
|
100
|
+
source: hosted
|
|
101
|
+
version: "3.0.1"
|
|
102
|
+
matcher:
|
|
103
|
+
dependency: transitive
|
|
104
|
+
description:
|
|
105
|
+
name: matcher
|
|
106
|
+
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
|
107
|
+
url: "https://pub.dev"
|
|
108
|
+
source: hosted
|
|
109
|
+
version: "0.12.16+1"
|
|
110
|
+
material_color_utilities:
|
|
111
|
+
dependency: transitive
|
|
112
|
+
description:
|
|
113
|
+
name: material_color_utilities
|
|
114
|
+
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
|
115
|
+
url: "https://pub.dev"
|
|
116
|
+
source: hosted
|
|
117
|
+
version: "0.11.1"
|
|
118
|
+
meta:
|
|
119
|
+
dependency: transitive
|
|
120
|
+
description:
|
|
121
|
+
name: meta
|
|
122
|
+
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
|
123
|
+
url: "https://pub.dev"
|
|
124
|
+
source: hosted
|
|
125
|
+
version: "1.15.0"
|
|
126
|
+
path:
|
|
127
|
+
dependency: transitive
|
|
128
|
+
description:
|
|
129
|
+
name: path
|
|
130
|
+
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
|
131
|
+
url: "https://pub.dev"
|
|
132
|
+
source: hosted
|
|
133
|
+
version: "1.9.0"
|
|
134
|
+
sky_engine:
|
|
135
|
+
dependency: transitive
|
|
136
|
+
description: flutter
|
|
137
|
+
source: sdk
|
|
138
|
+
version: "0.0.0"
|
|
139
|
+
source_span:
|
|
140
|
+
dependency: transitive
|
|
141
|
+
description:
|
|
142
|
+
name: source_span
|
|
143
|
+
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
|
144
|
+
url: "https://pub.dev"
|
|
145
|
+
source: hosted
|
|
146
|
+
version: "1.10.0"
|
|
147
|
+
stack_trace:
|
|
148
|
+
dependency: transitive
|
|
149
|
+
description:
|
|
150
|
+
name: stack_trace
|
|
151
|
+
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
|
152
|
+
url: "https://pub.dev"
|
|
153
|
+
source: hosted
|
|
154
|
+
version: "1.12.0"
|
|
155
|
+
stream_channel:
|
|
156
|
+
dependency: transitive
|
|
157
|
+
description:
|
|
158
|
+
name: stream_channel
|
|
159
|
+
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
|
160
|
+
url: "https://pub.dev"
|
|
161
|
+
source: hosted
|
|
162
|
+
version: "2.1.2"
|
|
163
|
+
string_scanner:
|
|
164
|
+
dependency: transitive
|
|
165
|
+
description:
|
|
166
|
+
name: string_scanner
|
|
167
|
+
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
|
168
|
+
url: "https://pub.dev"
|
|
169
|
+
source: hosted
|
|
170
|
+
version: "1.3.0"
|
|
171
|
+
term_glyph:
|
|
172
|
+
dependency: transitive
|
|
173
|
+
description:
|
|
174
|
+
name: term_glyph
|
|
175
|
+
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
|
176
|
+
url: "https://pub.dev"
|
|
177
|
+
source: hosted
|
|
178
|
+
version: "1.2.1"
|
|
179
|
+
test_api:
|
|
180
|
+
dependency: transitive
|
|
181
|
+
description:
|
|
182
|
+
name: test_api
|
|
183
|
+
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
|
184
|
+
url: "https://pub.dev"
|
|
185
|
+
source: hosted
|
|
186
|
+
version: "0.7.3"
|
|
187
|
+
typed_data:
|
|
188
|
+
dependency: transitive
|
|
189
|
+
description:
|
|
190
|
+
name: typed_data
|
|
191
|
+
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
|
192
|
+
url: "https://pub.dev"
|
|
193
|
+
source: hosted
|
|
194
|
+
version: "1.4.0"
|
|
195
|
+
vector_math:
|
|
196
|
+
dependency: transitive
|
|
197
|
+
description:
|
|
198
|
+
name: vector_math
|
|
199
|
+
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
|
200
|
+
url: "https://pub.dev"
|
|
201
|
+
source: hosted
|
|
202
|
+
version: "2.1.4"
|
|
203
|
+
vm_service:
|
|
204
|
+
dependency: transitive
|
|
205
|
+
description:
|
|
206
|
+
name: vm_service
|
|
207
|
+
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
|
208
|
+
url: "https://pub.dev"
|
|
209
|
+
source: hosted
|
|
210
|
+
version: "14.3.0"
|
|
211
|
+
web:
|
|
212
|
+
dependency: transitive
|
|
213
|
+
description:
|
|
214
|
+
name: web
|
|
215
|
+
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
|
216
|
+
url: "https://pub.dev"
|
|
217
|
+
source: hosted
|
|
218
|
+
version: "1.1.1"
|
|
219
|
+
sdks:
|
|
220
|
+
dart: ">=3.5.0 <4.0.0"
|
|
221
|
+
flutter: ">=3.18.0-18.0.pre.54"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: tacilytics
|
|
2
|
+
description: Privacy-first, cookieless app analytics for Tacilytics. No device id, no cookies, consent-aware.
|
|
3
|
+
version: 0.2.0
|
|
4
|
+
homepage: https://github.com/Open-Cosmic-Software/tacilytics
|
|
5
|
+
repository: https://github.com/Open-Cosmic-Software/tacilytics/tree/master/sdk/flutter
|
|
6
|
+
|
|
7
|
+
environment:
|
|
8
|
+
sdk: ">=3.0.0 <4.0.0"
|
|
9
|
+
flutter: ">=3.10.0"
|
|
10
|
+
|
|
11
|
+
dependencies:
|
|
12
|
+
flutter:
|
|
13
|
+
sdk: flutter
|
|
14
|
+
http: ">=0.13.0 <2.0.0"
|
|
15
|
+
|
|
16
|
+
dev_dependencies:
|
|
17
|
+
flutter_test:
|
|
18
|
+
sdk: flutter
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ignore_for_file: deprecated_member_use_from_same_package
|
|
2
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
3
|
+
import 'package:tacilytics/nyx_analytics.dart';
|
|
4
|
+
|
|
5
|
+
void main() {
|
|
6
|
+
test('old Nyx names still work after the rename', () async {
|
|
7
|
+
final c = NyxAnalytics.init(endpoint: 'http://127.0.0.1:9', domain: 'x.com', platformOverride: 'android');
|
|
8
|
+
expect(identical(c, Tacilytics.instance), isTrue);
|
|
9
|
+
expect(NyxBeacon.path('home'), '/home');
|
|
10
|
+
expect(NyxRouteObserver(), isA<TacilyticsRouteObserver>());
|
|
11
|
+
await c.screen('/home'); // disabled by default → returns quietly
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import 'dart:math';
|
|
2
|
+
|
|
3
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
4
|
+
import 'package:tacilytics/tacilytics.dart';
|
|
5
|
+
|
|
6
|
+
void main() {
|
|
7
|
+
test('screen names become paths', () {
|
|
8
|
+
expect(TacilyticsBeacon.path('product/42'), '/product/42');
|
|
9
|
+
expect(TacilyticsBeacon.path('/cart/'), '/cart');
|
|
10
|
+
expect(TacilyticsBeacon.path(' '), '/');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('session id is random, 24 url-safe chars', () {
|
|
14
|
+
final a = TacilyticsBeacon.newSessionId(Random(1)), b = TacilyticsBeacon.newSessionId(Random(2));
|
|
15
|
+
expect(a.length, 24);
|
|
16
|
+
expect(RegExp(r'^[A-Za-z0-9]+$').hasMatch(a), isTrue);
|
|
17
|
+
expect(a, isNot(b));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('properties are flat scalars, nested and null dropped', () {
|
|
21
|
+
final p = TacilyticsBeacon.props({'value': 49.9, 'items': 2, 'whole': 10.0, 'nested': {'a': 1}, 'list': [1], 'none': null});
|
|
22
|
+
expect(p, {'value': '49.90', 'items': '2', 'whole': '10'});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('build: pageview has no event fields, event carries props', () {
|
|
26
|
+
final pv = TacilyticsBeacon.build(domain: 'x.com', platform: 'android', sessionId: 's' * 24, screen: 'home', region: 'at');
|
|
27
|
+
expect(pv['p'], '/home');
|
|
28
|
+
expect(pv['cc'], 'AT');
|
|
29
|
+
expect(pv.containsKey('n'), isFalse);
|
|
30
|
+
expect(pv.containsKey('pr'), isFalse);
|
|
31
|
+
final ev = TacilyticsBeacon.build(domain: 'x.com', platform: 'ios', sessionId: 's' * 24, screen: '/cart', name: 'Purchase', properties: {'value': 12.5});
|
|
32
|
+
expect(ev['n'], 'Purchase');
|
|
33
|
+
expect(ev['pr'], {'value': '12.50'});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('invalid region is not sent', () {
|
|
37
|
+
final b = TacilyticsBeacon.build(domain: 'x.com', platform: 'ios', sessionId: 's' * 24, screen: '/', region: '419');
|
|
38
|
+
expect(b.containsKey('cc'), isFalse);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('client sends nothing until enabled', () async {
|
|
42
|
+
final c = Tacilytics.init(endpoint: 'http://127.0.0.1:9', domain: 'x.com', platformOverride: 'android');
|
|
43
|
+
expect(c.enabled, isFalse);
|
|
44
|
+
await c.screen('/home'); // must return quietly, no network attempt
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@Tags(['e2e'])
|
|
2
|
+
library;
|
|
3
|
+
|
|
4
|
+
import 'dart:io';
|
|
5
|
+
|
|
6
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
7
|
+
import 'package:tacilytics/tacilytics.dart';
|
|
8
|
+
|
|
9
|
+
// Runs against a live server: TACILYTICS_E2E=http://127.0.0.1:3999 flutter test --tags e2e
|
|
10
|
+
void main() {
|
|
11
|
+
final url = Platform.environment['TACILYTICS_E2E'];
|
|
12
|
+
test('beacons reach a running server', () async {
|
|
13
|
+
final c = Tacilytics.init(endpoint: url!, domain: 'pastaclean.tv', appVersion: '9.9.9-e2e', platformOverride: 'android', enabled: true);
|
|
14
|
+
await c.screen('/e2e/home');
|
|
15
|
+
await c.search('essig', 6);
|
|
16
|
+
await c.purchase(value: 12.5, items: 1);
|
|
17
|
+
}, skip: url == null ? 'set TACILYTICS_E2E' : false);
|
|
18
|
+
}
|