@proveanything/smartlinks 1.7.9 → 1.8.0

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.
@@ -0,0 +1,690 @@
1
+ # Collection Analytics
2
+
3
+ Build collection analytics dashboards and fire-and-forget tracking with the SmartLinks SDK.
4
+
5
+ This guide covers the `analytics` namespace, which is separate from `interactions`:
6
+
7
+ - Use `analytics` for generic web analytics, link clicks, QR landing pages, and tag scan telemetry
8
+ - Use `interactions` for structured user actions like votes, entries, submissions, and journey-triggering events
9
+
10
+ ---
11
+
12
+ ## Start Here
13
+
14
+ ### Use `analytics` when you want to...
15
+
16
+ - Track page views without waiting for a response
17
+ - Log outbound or internal link clicks
18
+ - Capture NFC / QR scan telemetry
19
+ - Build web analytics dashboards for a collection
20
+ - Query raw analytics events, time series, or grouped breakdowns
21
+
22
+ ### Use `interactions` when you want to...
23
+
24
+ - Define named interaction types per collection
25
+ - Count outcomes like votes or registrations
26
+ - Track user actions with explicit business meaning
27
+ - Feed journeys, competitions, or participation logic
28
+
29
+ ---
30
+
31
+ ## Overview
32
+
33
+ The `analytics` namespace has two layers:
34
+
35
+ | Layer | Purpose |
36
+ |---|---|
37
+ | Public ingestion | Fire-and-forget event logging from the browser or client runtime |
38
+ | Admin queries | Dashboard and reporting APIs for collection analytics |
39
+
40
+ There are two analytics domains:
41
+
42
+ | Domain | Route | Typical use |
43
+ |---|---|---|
44
+ | Collection events | `/public/analytics/collection` | Page views, clicks, app navigation, landing pages |
45
+ | Tag events | `/public/analytics/tag` | NFC / QR scans, claim/code activity, suspicious scan monitoring |
46
+
47
+ The backend stores custom analytics dimensions in `metadata`. For the most common attribution and placement keys, the public ingestion endpoints also accept standard top-level convenience fields and mirror them into `metadata` automatically.
48
+
49
+ See [docs/analytics-metadata-conventions.md](analytics-metadata-conventions.md) for the recommended key set.
50
+
51
+ ---
52
+
53
+ ## Quick Start
54
+
55
+ ### Track a page view
56
+
57
+ ```typescript
58
+ import { initializeApi, analytics } from '@proveanything/smartlinks'
59
+
60
+ initializeApi({ baseURL: 'https://smartlinks.app/api/v1' })
61
+
62
+ analytics.collection.track({
63
+ sessionId: 'sess_123',
64
+ eventType: 'page_view',
65
+ collectionId: 'demo-collection',
66
+ productId: 'product_1',
67
+ appId: 'homepage',
68
+ path: '/c/demo-collection',
69
+ deviceType: 'mobile',
70
+ })
71
+ ```
72
+
73
+ ### Track an outbound click
74
+
75
+ ```typescript
76
+ analytics.collection.track({
77
+ sessionId: 'sess_123',
78
+ eventType: 'click_link',
79
+ collectionId: 'demo-collection',
80
+ productId: 'product_1',
81
+ linkId: 'hero-cta',
82
+ destinationAppId: 'shop',
83
+ href: 'https://example.com/buy',
84
+ isExternal: true,
85
+ placement: 'hero',
86
+ campaign: 'summer-launch',
87
+ })
88
+ ```
89
+
90
+ ### Track a tag scan
91
+
92
+ ```typescript
93
+ analytics.tag.track({
94
+ sessionId: 'sess_123',
95
+ eventType: 'scan_tag',
96
+ collectionId: 'demo-collection',
97
+ productId: 'product_1',
98
+ codeId: 'code_123',
99
+ claimId: 'claim_456',
100
+ path: '/claim/code_123',
101
+ isAdmin: false,
102
+ location: { country: 'US' },
103
+ entryType: 'nfc',
104
+ scanMethod: 'nfc',
105
+ })
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Fire-and-Forget Tracking
111
+
112
+ `analytics.collection.track(...)` and `analytics.tag.track(...)` are designed for browser-friendly event logging.
113
+
114
+ They use this strategy:
115
+
116
+ 1. `navigator.sendBeacon()` when available
117
+ 2. fallback to `fetch(..., { keepalive: true })`
118
+ 3. do not wait for a response before continuing
119
+
120
+ This makes them a good fit for:
121
+
122
+ - page unload events
123
+ - click logging before navigation
124
+ - low-friction telemetry
125
+ - QR / NFC landing-page analytics
126
+
127
+ ### Return value
128
+
129
+ Both tracking helpers return a small synchronous result:
130
+
131
+ ```typescript
132
+ const result = analytics.collection.track({
133
+ eventType: 'page_view',
134
+ collectionId: 'demo-collection',
135
+ })
136
+
137
+ console.log(result)
138
+ // { queued: true, transport: 'beacon' }
139
+ ```
140
+
141
+ If you need a fully awaited request/response flow for business logic, use a regular admin or public API instead of fire-and-forget analytics tracking.
142
+
143
+ ---
144
+
145
+ ## Browser Helpers
146
+
147
+ If you are replacing older portal-style tracking code, the SDK now includes a small browser helper layer under `analytics.browser`.
148
+
149
+ These helpers are useful when you want the SDK to fill in common defaults such as:
150
+
151
+ - `sessionId`
152
+ - `visitorId`
153
+ - `deviceType`
154
+ - current `path`
155
+ - `pagePath`
156
+ - `referrer` and `referrerHost`
157
+ - campaign and query fields like `utmSource`, `utmCampaign`, `pageId`, and `qrCodeId`
158
+ - cached geolocation when you opt into it
159
+
160
+ Example:
161
+
162
+ ```typescript
163
+ analytics.browser.configure({
164
+ visitorId: 'shared-visitor-id',
165
+ defaultCollectionEvent: {
166
+ collectionId: 'demo-collection',
167
+ productId: 'product_1',
168
+ },
169
+ getCollectionDefaults: () => ({
170
+ appId: 'homepage',
171
+ }),
172
+ })
173
+
174
+ analytics.browser.trackPageView({
175
+ placement: 'hero',
176
+ })
177
+
178
+ analytics.browser.trackLinkClick({
179
+ href: 'https://example.com/buy',
180
+ linkId: 'hero-cta',
181
+ destinationAppId: 'shop',
182
+ })
183
+
184
+ console.log(analytics.browser.getVisitorId())
185
+
186
+ analytics.browser.setVisitorId('shared-visitor-id')
187
+ ```
188
+
189
+ If your apps and widgets run on the same domain, `visitorId` can be shared through browser storage. That means an outer shell can explicitly seed the ID once with either `analytics.browser.configure({ visitorId })` or `analytics.browser.setVisitorId(...)`, and inner apps can reuse it through `analytics.browser.getVisitorId()`.
190
+
191
+ ### Campaign and query capture
192
+
193
+ Browser helpers automatically capture common analytics query parameters such as:
194
+
195
+ - `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`
196
+ - `pageId`, `qrCodeId`
197
+ - `group`, `tag`, `campaign`, `placement`
198
+
199
+ You can also read them directly when building your own event shape:
200
+
201
+ ```typescript
202
+ const campaignFields = analytics.browser.captureCampaignParams()
203
+
204
+ analytics.browser.trackPageView({
205
+ collectionId: 'demo-collection',
206
+ productId: 'product_1',
207
+ ...campaignFields,
208
+ })
209
+ ```
210
+
211
+ ### SPA page-view tracking
212
+
213
+ For single-page apps, use `bindPageViews()` once during app startup to automatically track route changes.
214
+
215
+ ```typescript
216
+ const stopPageTracking = analytics.browser.bindPageViews({
217
+ trackInitialPageView: true,
218
+ event: {
219
+ collectionId: 'demo-collection',
220
+ productId: 'product_1',
221
+ appId: 'homepage',
222
+ },
223
+ })
224
+
225
+ // Later, if needed:
226
+ stopPageTracking()
227
+ ```
228
+
229
+ ### Delegated link tracking
230
+
231
+ For classic click analytics, use `bindLinkTracking()` instead of wiring individual anchors one by one.
232
+
233
+ ```typescript
234
+ const stopLinkTracking = analytics.browser.bindLinkTracking({
235
+ event: {
236
+ collectionId: 'demo-collection',
237
+ productId: 'product_1',
238
+ appId: 'homepage',
239
+ },
240
+ trackInternal: true,
241
+ })
242
+ ```
243
+
244
+ ### Opt-in geolocation
245
+
246
+ Automatic geolocation should stay opt-in because it depends on browser permission, product UX, and privacy requirements.
247
+
248
+ If you only need coarse regional data such as country, prefer the auth location endpoints instead of prompting for browser geolocation:
249
+
250
+ - use `auth.getAccount()` and read `account.location` when the user is authenticated
251
+ - use `auth.getLocation()` when the user is anonymous and you want best-effort IP-derived country/location data
252
+
253
+ `auth.getLocation()` is a good candidate for session caching. The SDK now caches it in session storage by default for 30 minutes, and you can tune or disable that behavior if needed.
254
+
255
+ ```typescript
256
+ const account = hasAuthCredentials()
257
+ ? await auth.getAccount()
258
+ : null
259
+
260
+ const location = account?.location ?? await auth.getLocation({
261
+ ttlMs: 30 * 60 * 1000,
262
+ })
263
+
264
+ analytics.browser.setLocation(location)
265
+
266
+ analytics.browser.trackTagScan({
267
+ collectionId: 'demo-collection',
268
+ productId: 'product_1',
269
+ codeId: 'code_123',
270
+ })
271
+ ```
272
+
273
+ If you later need to refresh that coarse location explicitly, call `auth.getLocation({ forceRefresh: true })` or clear it first with `auth.clearCachedLocation()`.
274
+
275
+ Use browser geolocation only when you actually need precise coordinates:
276
+
277
+ ```typescript
278
+ await analytics.browser.captureLocation({
279
+ enableHighAccuracy: false,
280
+ timeout: 3000,
281
+ includeCoordinates: true,
282
+ })
283
+ ```
284
+
285
+ If your app already has a trusted location source, you can set it directly with `analytics.browser.setLocation(...)` instead of asking the browser again.
286
+
287
+ ---
288
+
289
+ ## Public Ingestion
290
+
291
+ ### `analytics.collection.track(event)`
292
+
293
+ Tracks generic collection analytics events such as:
294
+
295
+ - `page_view`
296
+ - `click_link`
297
+ - QR landing-page visits
298
+ - internal navigation
299
+ - outbound link activity
300
+
301
+ Supported top-level fields include the core event fields plus standard convenience metadata fields such as `referrer`, `utmSource`, `group`, `placement`, `linkTitle`, `pagePath`, and `qrCodeId`.
302
+
303
+ `visitorId` is also supported as a standard top-level field and is mirrored into `metadata` by the backend for backward compatibility.
304
+
305
+ Example:
306
+
307
+ ```typescript
308
+ analytics.collection.track({
309
+ sessionId: 'sess_123',
310
+ eventType: 'page_view',
311
+ collectionId: 'demo-collection',
312
+ productId: 'product_1',
313
+ appId: 'homepage',
314
+ destinationAppId: 'shop',
315
+ linkId: 'hero-cta',
316
+ deviceType: 'mobile',
317
+ href: 'https://example.com/buy',
318
+ path: '/c/demo-collection',
319
+ isExternal: true,
320
+ location: { country: 'GB' },
321
+ referrerHost: 'instagram.com',
322
+ utmCampaign: 'summer-launch',
323
+ group: 'summer-launch',
324
+ placement: 'hero',
325
+ metadata: { pagePath: '/c/demo-collection?pageId=QR123' },
326
+ })
327
+ ```
328
+
329
+ ### `analytics.tag.track(event)`
330
+
331
+ Tracks physical scan analytics such as:
332
+
333
+ - `scan_tag`
334
+ - NFC or QR entry telemetry
335
+ - claim/code activity
336
+ - admin vs customer scan behavior
337
+
338
+ Supported top-level fields include the core scan fields plus the same standard convenience metadata fields, especially `entryType`, `scanMethod`, `group`, `tag`, and campaign or attribution keys when relevant.
339
+
340
+ Like collection events, tag events also accept `visitorId` as a standard top-level field.
341
+
342
+ Example:
343
+
344
+ ```typescript
345
+ analytics.tag.track({
346
+ sessionId: 'sess_123',
347
+ eventType: 'scan_tag',
348
+ collectionId: 'demo-collection',
349
+ productId: 'product_1',
350
+ codeId: 'code_123',
351
+ claimId: 'claim_456',
352
+ deviceType: 'mobile',
353
+ path: '/claim/code_123',
354
+ location: {
355
+ country: 'US',
356
+ latitude: 40.7,
357
+ longitude: -74.0,
358
+ area: 35,
359
+ },
360
+ isAdmin: false,
361
+ group: 'retail-batch-a',
362
+ tag: 'promo',
363
+ entryType: 'nfc',
364
+ scanMethod: 'nfc',
365
+ })
366
+ ```
367
+
368
+ Notes:
369
+
370
+ - `scan_blank_tag` is intentionally excluded by the backend analytics pipeline
371
+ - Admin scans may still trigger other workflows even when analytics logging is skipped
372
+
373
+ ### Queryable metadata
374
+
375
+ Top-level scalar metadata values are the most query-friendly today. You can filter them with `metadata` and break them down with `dimension: 'metadata'` plus `metadataKey`.
376
+
377
+ ```typescript
378
+ const grouped = await analytics.admin.breakdown('demo-collection', {
379
+ source: 'tag',
380
+ dimension: 'metadata',
381
+ metadataKey: 'group',
382
+ metric: 'count',
383
+ })
384
+ ```
385
+
386
+ ---
387
+
388
+ ## Admin Queries
389
+
390
+ All admin analytics routes are collection-scoped:
391
+
392
+ ```text
393
+ POST /api/v1/admin/collection/:collectionId/analytics/...
394
+ ```
395
+
396
+ ### Dashboard-style replacement endpoints
397
+
398
+ These are the easiest starting point if you are building dashboards similar to the older analytics app.
399
+
400
+ #### `analytics.admin.web(collectionId, body)`
401
+
402
+ Returns page-view oriented dashboard data.
403
+
404
+ ```typescript
405
+ const dashboard = await analytics.admin.web('demo-collection', {
406
+ startDate: '2026-03-01T00:00:00Z',
407
+ endDate: '2026-03-31T23:59:59Z',
408
+ productId: 'product_1',
409
+ })
410
+
411
+ console.log(dashboard.metrics.totalVisits)
412
+ console.log(dashboard.charts.topPages)
413
+ ```
414
+
415
+ #### `analytics.admin.clicks(collectionId, body)`
416
+
417
+ Returns click-focused dashboard data.
418
+
419
+ ```typescript
420
+ const clicks = await analytics.admin.clicks('demo-collection', {
421
+ startDate: '2026-03-01T00:00:00Z',
422
+ endDate: '2026-03-31T23:59:59Z',
423
+ })
424
+
425
+ console.log(clicks.metrics.totalClicks)
426
+ console.log(clicks.charts.topExternalLinks)
427
+ ```
428
+
429
+ #### `analytics.admin.tagScans(collectionId, body)`
430
+
431
+ Returns tag scan dashboards, risk scoring, and suspicious activity charts.
432
+
433
+ ```typescript
434
+ const scans = await analytics.admin.tagScans('demo-collection', {
435
+ productId: 'product_1',
436
+ startDate: '2026-03-01T00:00:00Z',
437
+ endDate: '2026-03-31T23:59:59Z',
438
+ })
439
+
440
+ console.log(scans.metrics.totalScans)
441
+ console.log(scans.charts.suspiciousTags)
442
+ ```
443
+
444
+ ### Classic web analytics shortcuts
445
+
446
+ If you are building a more traditional analytics UI, the SDK also includes opinionated wrappers for common reports:
447
+
448
+ - `analytics.admin.topPages(...)`
449
+ - `analytics.admin.topReferrers(...)`
450
+ - `analytics.admin.topCampaigns(...)`
451
+ - `analytics.admin.topSources(...)`
452
+ - `analytics.admin.topDestinations(...)`
453
+
454
+ Example:
455
+
456
+ ```typescript
457
+ const [pages, referrers, campaigns, destinations] = await Promise.all([
458
+ analytics.admin.topPages('demo-collection', { limit: 10 }),
459
+ analytics.admin.topReferrers('demo-collection', { limit: 10 }),
460
+ analytics.admin.topCampaigns('demo-collection', { limit: 10 }),
461
+ analytics.admin.topDestinations('demo-collection', { limit: 10 }),
462
+ ])
463
+
464
+ console.log(pages.rows)
465
+ console.log(referrers.rows)
466
+ console.log(campaigns.rows)
467
+ console.log(destinations.rows)
468
+ ```
469
+
470
+ #### `analytics.admin.products(collectionId, body)`
471
+
472
+ Returns distinct product IDs across both analytics domains.
473
+
474
+ ```typescript
475
+ const products = await analytics.admin.products('demo-collection')
476
+ console.log(products.products)
477
+ ```
478
+
479
+ #### `analytics.admin.qrCodes(collectionId, body)`
480
+
481
+ Returns QR/page identifiers extracted from event URLs.
482
+
483
+ ```typescript
484
+ const qrCodes = await analytics.admin.qrCodes('demo-collection')
485
+ console.log(qrCodes)
486
+ ```
487
+
488
+ #### `analytics.admin.tags(collectionId, body)`
489
+
490
+ Returns distinct tags with scan counts and active day counts.
491
+
492
+ ```typescript
493
+ const tags = await analytics.admin.tags('demo-collection')
494
+ console.log(tags.tags)
495
+ ```
496
+
497
+ ---
498
+
499
+ ## Generic Query Endpoints
500
+
501
+ Use these when you want charts, audits, or your own dashboard logic rather than the replacement dashboard responses.
502
+
503
+ ### Summary
504
+
505
+ ```typescript
506
+ const summary = await analytics.admin.summary('demo-collection', {
507
+ source: 'events',
508
+ eventType: 'page_view',
509
+ from: '2026-03-01T00:00:00Z',
510
+ to: '2026-03-31T23:59:59Z',
511
+ })
512
+
513
+ console.log(summary.summary.totalEvents)
514
+ console.log(summary.summary.uniqueSessions)
515
+ console.log(summary.summary.uniqueVisitors)
516
+ ```
517
+
518
+ ### Time series
519
+
520
+ ```typescript
521
+ const traffic = await analytics.admin.timeseries('demo-collection', {
522
+ source: 'events',
523
+ eventType: 'page_view',
524
+ granularity: 'week',
525
+ metric: 'uniqueVisitors',
526
+ from: '2026-02-01T00:00:00Z',
527
+ to: '2026-03-01T00:00:00Z',
528
+ })
529
+ ```
530
+
531
+ `uniqueVisitors` now works in generic analytics queries. The backend uses `visitorId` when present and falls back to `sessionId` for older events that do not include it yet.
532
+
533
+ ### Breakdown
534
+
535
+ ```typescript
536
+ const countries = await analytics.admin.breakdown('demo-collection', {
537
+ source: 'events',
538
+ eventType: 'page_view',
539
+ dimension: 'country',
540
+ metric: 'count',
541
+ limit: 25,
542
+ })
543
+ ```
544
+
545
+ ### Raw events
546
+
547
+ ```typescript
548
+ const rows = await analytics.admin.events('demo-collection', {
549
+ source: 'tag',
550
+ eventType: 'scan_tag',
551
+ from: '2026-03-01T00:00:00Z',
552
+ to: '2026-03-31T23:59:59Z',
553
+ limit: 100,
554
+ offset: 0,
555
+ sort: 'desc',
556
+ })
557
+ ```
558
+
559
+ ---
560
+
561
+ ## Legacy-Compatible Convenience Endpoints
562
+
563
+ The SDK exposes wrappers for the preserved compatibility routes too:
564
+
565
+ - `analytics.admin.weekly(...)`
566
+ - `analytics.admin.country(...)`
567
+
568
+ These are useful when migrating existing dashboards that still expect the older analytics app parameter shape.
569
+
570
+ ---
571
+
572
+ ## Common Filters
573
+
574
+ Most admin analytics queries support combinations of:
575
+
576
+ - `from`, `to`
577
+ - `eventType` or `eventTypes[]`
578
+ - `productId` or `productIds[]`
579
+ - `proofId` or `proofIds[]`
580
+ - `batchId` or `batchIds[]`
581
+ - `variantId` or `variantIds[]`
582
+ - `sessionId` or `sessionIds[]`
583
+ - `country` or `countries[]`
584
+ - `metadata` for top-level JSON equality matching
585
+
586
+ For metrics, generic queries support:
587
+
588
+ - `count`
589
+ - `uniqueSessions`
590
+ - `uniqueVisitors`
591
+
592
+ Extra collection-event filters include:
593
+
594
+ - `appId`, `appIds[]`
595
+ - `destinationAppId`, `destinationAppIds[]`
596
+ - `linkId`, `linkIds[]`
597
+ - `href`, `path`
598
+ - `hrefContains`, `pathContains`
599
+ - `isExternal`
600
+
601
+ Extra tag-event filters include:
602
+
603
+ - `codeId`, `codeIds[]`
604
+ - `claimId`, `claimIds[]`
605
+ - `isAdmin`
606
+ - `hasLocation`
607
+
608
+ ---
609
+
610
+ ## Best Practices
611
+
612
+ ### 1. Use analytics and interactions for different jobs
613
+
614
+ - `analytics` for telemetry, traffic, clicks, and scan monitoring
615
+ - `interactions` for explicit business events and outcomes
616
+
617
+ ### 2. Track before navigation
618
+
619
+ For outbound links, log the analytics event immediately before triggering navigation so `sendBeacon()` has the best chance of queuing it.
620
+
621
+ ### 3. Keep metadata shallow
622
+
623
+ Analytics metadata filtering currently works best with top-level scalar keys such as:
624
+
625
+ - `placement`
626
+ - `campaign`
627
+ - `group`
628
+ - `tag`
629
+ - `referrerHost`
630
+ - `utmSource`
631
+ - `utmCampaign`
632
+ - `pagePath`
633
+ - `scanMethod`
634
+
635
+ See [docs/analytics-metadata-conventions.md](analytics-metadata-conventions.md) for the recommended shared vocabulary.
636
+
637
+ ### 4. Prefer generic endpoints for custom dashboards
638
+
639
+ Use `/summary`, `/timeseries`, `/breakdown`, and `/events` when you are building your own reporting UI. Use `web`, `clicks`, and `tagScans` when you want opinionated dashboard responses.
640
+
641
+ ---
642
+
643
+ ## Example: Outbound Link Tracking
644
+
645
+ ```typescript
646
+ function trackAndNavigate(href: string) {
647
+ analytics.collection.track({
648
+ sessionId: 'sess_123',
649
+ eventType: 'click_link',
650
+ collectionId: 'demo-collection',
651
+ linkId: 'buy-now',
652
+ href,
653
+ isExternal: true,
654
+ placement: 'hero',
655
+ pagePath: '/c/demo-collection',
656
+ })
657
+
658
+ window.location.href = href
659
+ }
660
+ ```
661
+
662
+ ## Example: Scan Dashboard
663
+
664
+ ```typescript
665
+ async function loadScanDashboard(collectionId: string) {
666
+ const [summary, countries, suspicious] = await Promise.all([
667
+ analytics.admin.summary(collectionId, {
668
+ source: 'tag',
669
+ eventType: 'scan_tag',
670
+ }),
671
+ analytics.admin.breakdown(collectionId, {
672
+ source: 'tag',
673
+ eventType: 'scan_tag',
674
+ dimension: 'country',
675
+ metric: 'count',
676
+ limit: 10,
677
+ }),
678
+ analytics.admin.tagScans(collectionId, {
679
+ startDate: '2026-03-01T00:00:00Z',
680
+ endDate: '2026-03-31T23:59:59Z',
681
+ }),
682
+ ])
683
+
684
+ return {
685
+ totalScans: summary.summary.totalEvents,
686
+ topCountries: countries.rows,
687
+ suspiciousTags: suspicious.charts.suspiciousTags,
688
+ }
689
+ }
690
+ ```
@@ -2,6 +2,8 @@
2
2
 
3
3
  The `interactions` namespace is a **critical pattern** for tracking user engagement. Many apps rely heavily on it for logging events that can trigger journeys, feed analytics dashboards, and drive aggregated results such as vote counts.
4
4
 
5
+ If you want generic page analytics, outbound click tracking, QR landing telemetry, or tag scan dashboards, use [analytics.md](analytics.md) instead. `interactions` is for structured business events and outcomes, not general web analytics.
6
+
5
7
  ---
6
8
 
7
9
  ## Overview
@@ -47,6 +47,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
47
47
  | **API Reference** | `docs/API_SUMMARY.md` | Complete SDK function reference, types, error handling |
48
48
  | **Multi-Page Architecture** | `docs/mpa.md` | Build pipeline, entry points, multi-page setup, content hashing |
49
49
  | **AI & Chat** | `docs/ai.md` | Chat completions, RAG, streaming, tool calling, voice, podcasts, TTS |
50
+ | **Analytics** | `docs/analytics.md` | Fire-and-forget page/click/tag analytics plus admin dashboard queries |
50
51
  | **Theming** | `docs/theme.system.md` | Implementing dynamic themes via URL params or postMessage |
51
52
  | **Theme Defaults** | `docs/theme-defaults.md` | Default colour values for light/dark modes |
52
53
  | **Internationalization** | `docs/i18n.md` | Adding multi-language support, translation patterns |
@@ -54,7 +55,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
54
55
  | **Containers** | `docs/containers.md` | Building full-app embeddable containers (lazy-loaded) |
55
56
  | **Executors** | `docs/executor.md` | Building executor bundles for SEO, LLM content, programmatic config |
56
57
  | **Deep Linking** | `docs/deep-link-discovery.md` | URL state management, navigable states, portal menus, AI nav |
57
- | **Interactions** | `docs/interactions.md` | Event tracking, analytics, voting, competitions, journey triggers |
58
+ | **Interactions** | `docs/interactions.md` | Business events, outcomes, voting, competitions, and journey triggers |
58
59
  | **AI-Native Manifests** | `docs/manifests.md` | `app.manifest.json`, `app.admin.json`, `ai-guide.md` structure |
59
60
  | **App Config Files** | `docs/app-manifest.md` | Full field-by-field reference for both JSON config files |
60
61
  | **Real-time Messaging** | `docs/realtime.md` | Adding Ably real-time features (chat, live updates) |