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.
Files changed (307) hide show
  1. package/.env.example +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +253 -2
  4. package/cli.js +173 -0
  5. package/day-salt.js +55 -0
  6. package/package.json +56 -4
  7. package/public/app.js +788 -0
  8. package/public/assets/DISCLAIMER-simple-icons.md +51 -0
  9. package/public/assets/LICENSE-lucide.txt +43 -0
  10. package/public/assets/LICENSE-simple-icons.md +30 -0
  11. package/public/assets/cosmic-bg.webp +0 -0
  12. package/public/assets/flags/LICENSE-flag-icons.txt +21 -0
  13. package/public/assets/flags/ad.svg +150 -0
  14. package/public/assets/flags/ae.svg +6 -0
  15. package/public/assets/flags/af.svg +81 -0
  16. package/public/assets/flags/ag.svg +14 -0
  17. package/public/assets/flags/ai.svg +29 -0
  18. package/public/assets/flags/al.svg +5 -0
  19. package/public/assets/flags/am.svg +5 -0
  20. package/public/assets/flags/ao.svg +13 -0
  21. package/public/assets/flags/aq.svg +5 -0
  22. package/public/assets/flags/ar.svg +32 -0
  23. package/public/assets/flags/arab.svg +109 -0
  24. package/public/assets/flags/as.svg +72 -0
  25. package/public/assets/flags/asean.svg +13 -0
  26. package/public/assets/flags/at.svg +4 -0
  27. package/public/assets/flags/au.svg +8 -0
  28. package/public/assets/flags/aw.svg +186 -0
  29. package/public/assets/flags/ax.svg +18 -0
  30. package/public/assets/flags/az.svg +8 -0
  31. package/public/assets/flags/ba.svg +12 -0
  32. package/public/assets/flags/bb.svg +6 -0
  33. package/public/assets/flags/bd.svg +4 -0
  34. package/public/assets/flags/be.svg +7 -0
  35. package/public/assets/flags/bf.svg +7 -0
  36. package/public/assets/flags/bg.svg +5 -0
  37. package/public/assets/flags/bh.svg +4 -0
  38. package/public/assets/flags/bi.svg +15 -0
  39. package/public/assets/flags/bj.svg +14 -0
  40. package/public/assets/flags/bl.svg +5 -0
  41. package/public/assets/flags/bm.svg +97 -0
  42. package/public/assets/flags/bn.svg +36 -0
  43. package/public/assets/flags/bo.svg +673 -0
  44. package/public/assets/flags/bq.svg +5 -0
  45. package/public/assets/flags/br.svg +45 -0
  46. package/public/assets/flags/bs.svg +13 -0
  47. package/public/assets/flags/bt.svg +89 -0
  48. package/public/assets/flags/bv.svg +13 -0
  49. package/public/assets/flags/bw.svg +7 -0
  50. package/public/assets/flags/by.svg +18 -0
  51. package/public/assets/flags/bz.svg +145 -0
  52. package/public/assets/flags/ca.svg +4 -0
  53. package/public/assets/flags/cc.svg +19 -0
  54. package/public/assets/flags/cd.svg +5 -0
  55. package/public/assets/flags/cefta.svg +13 -0
  56. package/public/assets/flags/cf.svg +15 -0
  57. package/public/assets/flags/cg.svg +12 -0
  58. package/public/assets/flags/ch.svg +9 -0
  59. package/public/assets/flags/ci.svg +7 -0
  60. package/public/assets/flags/ck.svg +9 -0
  61. package/public/assets/flags/cl.svg +13 -0
  62. package/public/assets/flags/cm.svg +15 -0
  63. package/public/assets/flags/cn.svg +11 -0
  64. package/public/assets/flags/co.svg +7 -0
  65. package/public/assets/flags/cp.svg +7 -0
  66. package/public/assets/flags/cr.svg +7 -0
  67. package/public/assets/flags/cu.svg +13 -0
  68. package/public/assets/flags/cv.svg +13 -0
  69. package/public/assets/flags/cw.svg +14 -0
  70. package/public/assets/flags/cx.svg +15 -0
  71. package/public/assets/flags/cy.svg +6 -0
  72. package/public/assets/flags/cz.svg +5 -0
  73. package/public/assets/flags/de.svg +5 -0
  74. package/public/assets/flags/dg.svg +130 -0
  75. package/public/assets/flags/dj.svg +13 -0
  76. package/public/assets/flags/dk.svg +5 -0
  77. package/public/assets/flags/dm.svg +152 -0
  78. package/public/assets/flags/do.svg +121 -0
  79. package/public/assets/flags/dz.svg +5 -0
  80. package/public/assets/flags/eac.svg +48 -0
  81. package/public/assets/flags/ec.svg +138 -0
  82. package/public/assets/flags/ee.svg +5 -0
  83. package/public/assets/flags/eg.svg +38 -0
  84. package/public/assets/flags/eh.svg +16 -0
  85. package/public/assets/flags/er.svg +8 -0
  86. package/public/assets/flags/es-ct.svg +4 -0
  87. package/public/assets/flags/es-ga.svg +187 -0
  88. package/public/assets/flags/es-pv.svg +5 -0
  89. package/public/assets/flags/es.svg +544 -0
  90. package/public/assets/flags/et.svg +14 -0
  91. package/public/assets/flags/eu.svg +28 -0
  92. package/public/assets/flags/fi.svg +5 -0
  93. package/public/assets/flags/fj.svg +120 -0
  94. package/public/assets/flags/fk.svg +90 -0
  95. package/public/assets/flags/fm.svg +11 -0
  96. package/public/assets/flags/fo.svg +12 -0
  97. package/public/assets/flags/fr.svg +5 -0
  98. package/public/assets/flags/ga.svg +7 -0
  99. package/public/assets/flags/gb-eng.svg +5 -0
  100. package/public/assets/flags/gb-nir.svg +132 -0
  101. package/public/assets/flags/gb-sct.svg +4 -0
  102. package/public/assets/flags/gb-wls.svg +9 -0
  103. package/public/assets/flags/gb.svg +7 -0
  104. package/public/assets/flags/gd.svg +27 -0
  105. package/public/assets/flags/ge.svg +6 -0
  106. package/public/assets/flags/gf.svg +5 -0
  107. package/public/assets/flags/gg.svg +9 -0
  108. package/public/assets/flags/gh.svg +6 -0
  109. package/public/assets/flags/gi.svg +32 -0
  110. package/public/assets/flags/gl.svg +4 -0
  111. package/public/assets/flags/gm.svg +14 -0
  112. package/public/assets/flags/gn.svg +7 -0
  113. package/public/assets/flags/gp.svg +5 -0
  114. package/public/assets/flags/gq.svg +23 -0
  115. package/public/assets/flags/gr.svg +16 -0
  116. package/public/assets/flags/gs.svg +133 -0
  117. package/public/assets/flags/gt.svg +204 -0
  118. package/public/assets/flags/gu.svg +19 -0
  119. package/public/assets/flags/gw.svg +13 -0
  120. package/public/assets/flags/gy.svg +9 -0
  121. package/public/assets/flags/hk.svg +8 -0
  122. package/public/assets/flags/hm.svg +8 -0
  123. package/public/assets/flags/hn.svg +18 -0
  124. package/public/assets/flags/hr.svg +58 -0
  125. package/public/assets/flags/ht.svg +116 -0
  126. package/public/assets/flags/hu.svg +7 -0
  127. package/public/assets/flags/ic.svg +7 -0
  128. package/public/assets/flags/id.svg +4 -0
  129. package/public/assets/flags/ie.svg +7 -0
  130. package/public/assets/flags/il.svg +14 -0
  131. package/public/assets/flags/im.svg +36 -0
  132. package/public/assets/flags/in.svg +25 -0
  133. package/public/assets/flags/io.svg +130 -0
  134. package/public/assets/flags/iq.svg +10 -0
  135. package/public/assets/flags/ir.svg +219 -0
  136. package/public/assets/flags/is.svg +12 -0
  137. package/public/assets/flags/it.svg +7 -0
  138. package/public/assets/flags/je.svg +62 -0
  139. package/public/assets/flags/jm.svg +8 -0
  140. package/public/assets/flags/jo.svg +16 -0
  141. package/public/assets/flags/jp.svg +11 -0
  142. package/public/assets/flags/ke.svg +23 -0
  143. package/public/assets/flags/kg.svg +4 -0
  144. package/public/assets/flags/kh.svg +61 -0
  145. package/public/assets/flags/ki.svg +36 -0
  146. package/public/assets/flags/km.svg +16 -0
  147. package/public/assets/flags/kn.svg +14 -0
  148. package/public/assets/flags/kp.svg +15 -0
  149. package/public/assets/flags/kr.svg +24 -0
  150. package/public/assets/flags/kw.svg +13 -0
  151. package/public/assets/flags/ky.svg +103 -0
  152. package/public/assets/flags/kz.svg +36 -0
  153. package/public/assets/flags/la.svg +12 -0
  154. package/public/assets/flags/lb.svg +15 -0
  155. package/public/assets/flags/lc.svg +8 -0
  156. package/public/assets/flags/li.svg +43 -0
  157. package/public/assets/flags/lk.svg +22 -0
  158. package/public/assets/flags/lr.svg +14 -0
  159. package/public/assets/flags/ls.svg +8 -0
  160. package/public/assets/flags/lt.svg +7 -0
  161. package/public/assets/flags/lu.svg +5 -0
  162. package/public/assets/flags/lv.svg +6 -0
  163. package/public/assets/flags/ly.svg +13 -0
  164. package/public/assets/flags/ma.svg +4 -0
  165. package/public/assets/flags/mc.svg +6 -0
  166. package/public/assets/flags/md.svg +70 -0
  167. package/public/assets/flags/me.svg +116 -0
  168. package/public/assets/flags/mf.svg +5 -0
  169. package/public/assets/flags/mg.svg +7 -0
  170. package/public/assets/flags/mh.svg +7 -0
  171. package/public/assets/flags/mk.svg +5 -0
  172. package/public/assets/flags/ml.svg +7 -0
  173. package/public/assets/flags/mm.svg +12 -0
  174. package/public/assets/flags/mn.svg +14 -0
  175. package/public/assets/flags/mo.svg +9 -0
  176. package/public/assets/flags/mp.svg +86 -0
  177. package/public/assets/flags/mq.svg +5 -0
  178. package/public/assets/flags/mr.svg +6 -0
  179. package/public/assets/flags/ms.svg +29 -0
  180. package/public/assets/flags/mt.svg +58 -0
  181. package/public/assets/flags/mu.svg +8 -0
  182. package/public/assets/flags/mv.svg +6 -0
  183. package/public/assets/flags/mw.svg +10 -0
  184. package/public/assets/flags/mx.svg +382 -0
  185. package/public/assets/flags/my.svg +26 -0
  186. package/public/assets/flags/mz.svg +21 -0
  187. package/public/assets/flags/na.svg +16 -0
  188. package/public/assets/flags/nc.svg +13 -0
  189. package/public/assets/flags/ne.svg +6 -0
  190. package/public/assets/flags/nf.svg +9 -0
  191. package/public/assets/flags/ng.svg +6 -0
  192. package/public/assets/flags/ni.svg +129 -0
  193. package/public/assets/flags/nl.svg +5 -0
  194. package/public/assets/flags/no.svg +7 -0
  195. package/public/assets/flags/np.svg +13 -0
  196. package/public/assets/flags/nr.svg +12 -0
  197. package/public/assets/flags/nu.svg +10 -0
  198. package/public/assets/flags/nz.svg +36 -0
  199. package/public/assets/flags/om.svg +115 -0
  200. package/public/assets/flags/pa.svg +14 -0
  201. package/public/assets/flags/pc.svg +33 -0
  202. package/public/assets/flags/pe.svg +4 -0
  203. package/public/assets/flags/pf.svg +19 -0
  204. package/public/assets/flags/pg.svg +9 -0
  205. package/public/assets/flags/ph.svg +6 -0
  206. package/public/assets/flags/pk.svg +15 -0
  207. package/public/assets/flags/pl.svg +6 -0
  208. package/public/assets/flags/pm.svg +5 -0
  209. package/public/assets/flags/pn.svg +53 -0
  210. package/public/assets/flags/pr.svg +13 -0
  211. package/public/assets/flags/ps.svg +6 -0
  212. package/public/assets/flags/pt.svg +57 -0
  213. package/public/assets/flags/pw.svg +11 -0
  214. package/public/assets/flags/py.svg +157 -0
  215. package/public/assets/flags/qa.svg +4 -0
  216. package/public/assets/flags/re.svg +5 -0
  217. package/public/assets/flags/ro.svg +7 -0
  218. package/public/assets/flags/rs.svg +292 -0
  219. package/public/assets/flags/ru.svg +5 -0
  220. package/public/assets/flags/rw.svg +13 -0
  221. package/public/assets/flags/sa.svg +25 -0
  222. package/public/assets/flags/sb.svg +13 -0
  223. package/public/assets/flags/sc.svg +7 -0
  224. package/public/assets/flags/sd.svg +13 -0
  225. package/public/assets/flags/se.svg +4 -0
  226. package/public/assets/flags/sg.svg +13 -0
  227. package/public/assets/flags/sh-ac.svg +689 -0
  228. package/public/assets/flags/sh-hl.svg +164 -0
  229. package/public/assets/flags/sh-ta.svg +76 -0
  230. package/public/assets/flags/sh.svg +7 -0
  231. package/public/assets/flags/si.svg +18 -0
  232. package/public/assets/flags/sj.svg +7 -0
  233. package/public/assets/flags/sk.svg +9 -0
  234. package/public/assets/flags/sl.svg +7 -0
  235. package/public/assets/flags/sm.svg +75 -0
  236. package/public/assets/flags/sn.svg +8 -0
  237. package/public/assets/flags/so.svg +11 -0
  238. package/public/assets/flags/sr.svg +6 -0
  239. package/public/assets/flags/ss.svg +8 -0
  240. package/public/assets/flags/st.svg +16 -0
  241. package/public/assets/flags/sv.svg +593 -0
  242. package/public/assets/flags/sx.svg +56 -0
  243. package/public/assets/flags/sy.svg +6 -0
  244. package/public/assets/flags/sz.svg +34 -0
  245. package/public/assets/flags/tc.svg +50 -0
  246. package/public/assets/flags/td.svg +7 -0
  247. package/public/assets/flags/tf.svg +15 -0
  248. package/public/assets/flags/tg.svg +14 -0
  249. package/public/assets/flags/th.svg +7 -0
  250. package/public/assets/flags/tj.svg +22 -0
  251. package/public/assets/flags/tk.svg +5 -0
  252. package/public/assets/flags/tl.svg +13 -0
  253. package/public/assets/flags/tm.svg +204 -0
  254. package/public/assets/flags/tn.svg +4 -0
  255. package/public/assets/flags/to.svg +10 -0
  256. package/public/assets/flags/tr.svg +8 -0
  257. package/public/assets/flags/tt.svg +5 -0
  258. package/public/assets/flags/tv.svg +9 -0
  259. package/public/assets/flags/tw.svg +34 -0
  260. package/public/assets/flags/tz.svg +13 -0
  261. package/public/assets/flags/ua.svg +6 -0
  262. package/public/assets/flags/ug.svg +30 -0
  263. package/public/assets/flags/um.svg +9 -0
  264. package/public/assets/flags/un.svg +16 -0
  265. package/public/assets/flags/us.svg +9 -0
  266. package/public/assets/flags/uy.svg +28 -0
  267. package/public/assets/flags/uz.svg +30 -0
  268. package/public/assets/flags/va.svg +190 -0
  269. package/public/assets/flags/vc.svg +8 -0
  270. package/public/assets/flags/ve.svg +26 -0
  271. package/public/assets/flags/vg.svg +59 -0
  272. package/public/assets/flags/vi.svg +28 -0
  273. package/public/assets/flags/vn.svg +11 -0
  274. package/public/assets/flags/vu.svg +21 -0
  275. package/public/assets/flags/wf.svg +5 -0
  276. package/public/assets/flags/ws.svg +7 -0
  277. package/public/assets/flags/xk.svg +5 -0
  278. package/public/assets/flags/xx.svg +4 -0
  279. package/public/assets/flags/ye.svg +7 -0
  280. package/public/assets/flags/yt.svg +5 -0
  281. package/public/assets/flags/za.svg +17 -0
  282. package/public/assets/flags/zm.svg +27 -0
  283. package/public/assets/flags/zw.svg +21 -0
  284. package/public/assets/fonts/Inter-OFL.txt +93 -0
  285. package/public/assets/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  286. package/public/assets/fonts/inter-latin-wght-normal.woff2 +0 -0
  287. package/public/assets/icons.svg +200 -0
  288. package/public/assets/nyx-logo-256.png +0 -0
  289. package/public/assets/tacilytics-mark.svg +16 -0
  290. package/public/index.html +239 -0
  291. package/public/style.css +415 -0
  292. package/public/tacilytics.js +184 -0
  293. package/sdk/flutter/CHANGELOG.md +6 -0
  294. package/sdk/flutter/LICENSE +21 -0
  295. package/sdk/flutter/README.md +41 -0
  296. package/sdk/flutter/dart_test.yaml +3 -0
  297. package/sdk/flutter/lib/nyx_analytics.dart +13 -0
  298. package/sdk/flutter/lib/src/beacon.dart +87 -0
  299. package/sdk/flutter/lib/src/client.dart +177 -0
  300. package/sdk/flutter/lib/tacilytics.dart +13 -0
  301. package/sdk/flutter/pubspec.lock +221 -0
  302. package/sdk/flutter/pubspec.yaml +18 -0
  303. package/sdk/flutter/test/alias_test.dart +13 -0
  304. package/sdk/flutter/test/beacon_test.dart +46 -0
  305. package/sdk/flutter/test/e2e_test.dart +18 -0
  306. package/server.js +896 -0
  307. 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,3 @@
1
+ tags:
2
+ e2e:
3
+ skip: "live server test — run with TACILYTICS_E2E=<url> flutter test --tags e2e --run-skipped"
@@ -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
+ }