@proveanything/smartlinks 1.8.0 → 1.8.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/docs/analytics.md CHANGED
@@ -44,7 +44,7 @@ There are two analytics domains:
44
44
  | Collection events | `/public/analytics/collection` | Page views, clicks, app navigation, landing pages |
45
45
  | Tag events | `/public/analytics/tag` | NFC / QR scans, claim/code activity, suspicious scan monitoring |
46
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.
47
+ The backend stores custom analytics dimensions in `metadata`, but promoted analytics fields now belong at top level and are queried from real columns.
48
48
 
49
49
  See [docs/analytics-metadata-conventions.md](analytics-metadata-conventions.md) for the recommended key set.
50
50
 
@@ -60,7 +60,7 @@ import { initializeApi, analytics } from '@proveanything/smartlinks'
60
60
  initializeApi({ baseURL: 'https://smartlinks.app/api/v1' })
61
61
 
62
62
  analytics.collection.track({
63
- sessionId: 'sess_123',
63
+ sessionId: 1234567890,
64
64
  eventType: 'page_view',
65
65
  collectionId: 'demo-collection',
66
66
  productId: 'product_1',
@@ -74,7 +74,7 @@ analytics.collection.track({
74
74
 
75
75
  ```typescript
76
76
  analytics.collection.track({
77
- sessionId: 'sess_123',
77
+ sessionId: 1234567890,
78
78
  eventType: 'click_link',
79
79
  collectionId: 'demo-collection',
80
80
  productId: 'product_1',
@@ -91,7 +91,7 @@ analytics.collection.track({
91
91
 
92
92
  ```typescript
93
93
  analytics.tag.track({
94
- sessionId: 'sess_123',
94
+ sessionId: 1234567890,
95
95
  eventType: 'scan_tag',
96
96
  collectionId: 'demo-collection',
97
97
  productId: 'product_1',
@@ -298,15 +298,13 @@ Tracks generic collection analytics events such as:
298
298
  - internal navigation
299
299
  - outbound link activity
300
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.
301
+ Supported top-level fields include the core event fields plus promoted analytics columns such as `visitorId`, `referrerHost`, `pageId`, and `entryType`, along with custom metadata dimensions like `group`, `placement`, `pagePath`, and `qrCodeId`.
304
302
 
305
303
  Example:
306
304
 
307
305
  ```typescript
308
306
  analytics.collection.track({
309
- sessionId: 'sess_123',
307
+ sessionId: 1234567890,
310
308
  eventType: 'page_view',
311
309
  collectionId: 'demo-collection',
312
310
  productId: 'product_1',
@@ -322,6 +320,7 @@ analytics.collection.track({
322
320
  utmCampaign: 'summer-launch',
323
321
  group: 'summer-launch',
324
322
  placement: 'hero',
323
+ pageId: 'QR123',
325
324
  metadata: { pagePath: '/c/demo-collection?pageId=QR123' },
326
325
  })
327
326
  ```
@@ -335,15 +334,13 @@ Tracks physical scan analytics such as:
335
334
  - claim/code activity
336
335
  - admin vs customer scan behavior
337
336
 
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.
337
+ Supported top-level fields include the core scan fields plus promoted analytics columns such as `visitorId`, `entryType`, and `scanMethod`, along with custom metadata dimensions like `group`, `tag`, and campaign extras.
341
338
 
342
339
  Example:
343
340
 
344
341
  ```typescript
345
342
  analytics.tag.track({
346
- sessionId: 'sess_123',
343
+ sessionId: 1234567890,
347
344
  eventType: 'scan_tag',
348
345
  collectionId: 'demo-collection',
349
346
  productId: 'product_1',
@@ -374,6 +371,8 @@ Notes:
374
371
 
375
372
  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
373
 
374
+ Promoted fields such as `visitorId`, `referrerHost`, `pageId`, `entryType`, and `scanMethod` should be sent and queried as top-level fields, not inside `metadata`.
375
+
377
376
  ```typescript
378
377
  const grouped = await analytics.admin.breakdown('demo-collection', {
379
378
  source: 'tag',
@@ -528,7 +527,7 @@ const traffic = await analytics.admin.timeseries('demo-collection', {
528
527
  })
529
528
  ```
530
529
 
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.
530
+ `uniqueVisitors` now works in generic analytics queries. The backend uses the top-level `visitorId` column when present and falls back to numeric `sessionId` for older events that do not include it yet.
532
531
 
533
532
  ### Breakdown
534
533
 
@@ -579,7 +578,7 @@ Most admin analytics queries support combinations of:
579
578
  - `proofId` or `proofIds[]`
580
579
  - `batchId` or `batchIds[]`
581
580
  - `variantId` or `variantIds[]`
582
- - `sessionId` or `sessionIds[]`
581
+ - `sessionId` or `sessionIds[]` as numbers
583
582
  - `country` or `countries[]`
584
583
  - `metadata` for top-level JSON equality matching
585
584
 
@@ -626,11 +625,9 @@ Analytics metadata filtering currently works best with top-level scalar keys suc
626
625
  - `campaign`
627
626
  - `group`
628
627
  - `tag`
629
- - `referrerHost`
630
628
  - `utmSource`
631
629
  - `utmCampaign`
632
630
  - `pagePath`
633
- - `scanMethod`
634
631
 
635
632
  See [docs/analytics-metadata-conventions.md](analytics-metadata-conventions.md) for the recommended shared vocabulary.
636
633
 
@@ -645,7 +642,7 @@ Use `/summary`, `/timeseries`, `/breakdown`, and `/events` when you are building
645
642
  ```typescript
646
643
  function trackAndNavigate(href: string) {
647
644
  analytics.collection.track({
648
- sessionId: 'sess_123',
645
+ sessionId: 1234567890,
649
646
  eventType: 'click_link',
650
647
  collectionId: 'demo-collection',
651
648
  linkId: 'buy-now',
@@ -0,0 +1,308 @@
1
+ # Iframe Streaming Parent Changes
2
+
3
+ This note describes the parent-side changes needed to support AI streaming when an embedded SmartLinks app is running in iframe proxy mode.
4
+
5
+ If you are using the SDK `IframeResponder` directly, this is already implemented in the SDK changes. You only need this document if your parent application has its own iframe proxy handler and does not rely on `IframeResponder`.
6
+
7
+ ## Goal
8
+
9
+ Keep the existing architecture:
10
+
11
+ - local mode: child calls API directly
12
+ - iframe proxy mode: child never owns auth state and streams through the parent
13
+
14
+ This keeps user/session authority in the parent while making AI streaming behave like the rest of the SDK transport.
15
+
16
+ ## What changed
17
+
18
+ Previously, proxy mode only supported one-shot request/response messages:
19
+
20
+ - `_smartlinksProxyRequest`
21
+ - `_smartlinksProxyResponse`
22
+
23
+ Streaming now adds a second protocol for long-lived responses:
24
+
25
+ - `_smartlinksProxyStreamRequest`
26
+ - `_smartlinksProxyStream`
27
+ - `_smartlinksProxyStreamAbort`
28
+
29
+ ## New parent message handling
30
+
31
+ ### 1. Listen for stream requests
32
+
33
+ The iframe child may now send this message:
34
+
35
+ ```ts
36
+ {
37
+ _smartlinksProxyStreamRequest: true,
38
+ id: string,
39
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
40
+ path: string,
41
+ body?: any,
42
+ headers?: Record<string, string>
43
+ }
44
+ ```
45
+
46
+ Parent behavior:
47
+
48
+ - treat this like a proxied API request
49
+ - build the real API URL from your configured base URL plus `path`
50
+ - send the request using the parent's current auth/session context
51
+ - expect an SSE / streaming response body
52
+ - keep the request open until the stream ends or is aborted
53
+
54
+ ### 2. Forward stream lifecycle messages back to the child
55
+
56
+ The parent should send messages back to the iframe using this envelope:
57
+
58
+ ```ts
59
+ {
60
+ _smartlinksProxyStream: true,
61
+ id: string,
62
+ phase: 'open' | 'event' | 'end' | 'error',
63
+ data?: any,
64
+ error?: string,
65
+ status?: number
66
+ }
67
+ ```
68
+
69
+ Phases:
70
+
71
+ - `open`
72
+ - optional but recommended
73
+ - indicates the upstream streaming request was accepted and a body exists
74
+ - `event`
75
+ - contains one parsed JSON event from an SSE `data:` frame
76
+ - send one message per logical event payload
77
+ - `end`
78
+ - sent once when the stream finishes normally
79
+ - `error`
80
+ - sent if the upstream request fails before or during streaming
81
+
82
+ ### 3. Support abort from the child
83
+
84
+ The child may stop reading early and send:
85
+
86
+ ```ts
87
+ {
88
+ _smartlinksProxyStreamAbort: true,
89
+ id: string
90
+ }
91
+ ```
92
+
93
+ Parent behavior:
94
+
95
+ - look up the active stream by `id`
96
+ - abort the underlying fetch / reader
97
+ - clean up any local state for that stream
98
+ - do not keep streaming after abort
99
+
100
+ ## SSE forwarding rules
101
+
102
+ The upstream AI endpoints return SSE-like frames. The parent should:
103
+
104
+ - read the response body as a stream
105
+ - buffer text until line boundaries
106
+ - collect `data:` lines for a single event
107
+ - join multi-line `data:` payloads with `\n`
108
+ - ignore blank events
109
+ - stop on `data: [DONE]`
110
+ - JSON-parse each event payload
111
+ - forward parsed payloads to the iframe as `_smartlinksProxyStream` with `phase: 'event'`
112
+
113
+ Minimal parsing behavior:
114
+
115
+ 1. accumulate bytes into text
116
+ 2. split on `\r?\n`
117
+ 3. collect each `data:` line
118
+ 4. on blank line, finalize the event
119
+ 5. if payload is `[DONE]`, finish
120
+ 6. otherwise `JSON.parse(payload)` and forward
121
+
122
+ ## Auth and session expectations
123
+
124
+ The parent remains the source of truth for auth.
125
+
126
+ That means the parent stream handler should:
127
+
128
+ - use the same auth headers/token source as normal proxied requests
129
+ - not require the iframe to know the bearer token or API key
130
+ - naturally pick up the current logged-in user when the stream starts
131
+ - cancel active streams if your app invalidates session state on logout or account switch
132
+
133
+ In practice, the stream request should use the same header-building logic as your normal parent proxy transport.
134
+
135
+ ## Error handling expectations
136
+
137
+ If the upstream fetch returns a non-2xx status:
138
+
139
+ - try to read the JSON error body
140
+ - derive a useful message
141
+ - send one `_smartlinksProxyStream` message with `phase: 'error'`
142
+ - include `status` when available
143
+ - do not send `end` afterward
144
+
145
+ If the stream body is missing unexpectedly:
146
+
147
+ - send `phase: 'error'`
148
+
149
+ If JSON parsing fails for a single event chunk:
150
+
151
+ - safest behavior is to ignore that malformed chunk and continue
152
+
153
+ ## State the parent should keep
154
+
155
+ Track active streams in a map keyed by `id`:
156
+
157
+ ```ts
158
+ Map<string, AbortController>
159
+ ```
160
+
161
+ Recommended cleanup points:
162
+
163
+ - on normal stream end
164
+ - on error
165
+ - on child abort
166
+ - on iframe detach/unmount
167
+ - on parent auth reset/logout if you want all in-flight streams cancelled immediately
168
+
169
+ ## Parent implementation outline
170
+
171
+ ```ts
172
+ const activeStreams = new Map<string, AbortController>()
173
+
174
+ window.addEventListener('message', async (event) => {
175
+ const msg = event.data
176
+
177
+ if (msg?._smartlinksProxyStreamAbort && msg.id) {
178
+ activeStreams.get(msg.id)?.abort()
179
+ activeStreams.delete(msg.id)
180
+ return
181
+ }
182
+
183
+ if (msg?._smartlinksProxyStreamRequest && msg.id) {
184
+ const controller = new AbortController()
185
+ activeStreams.set(msg.id, controller)
186
+
187
+ try {
188
+ const response = await fetch(buildUrl(msg.path), {
189
+ method: msg.method,
190
+ headers: msg.headers,
191
+ body: msg.body ? JSON.stringify(msg.body) : undefined,
192
+ signal: controller.signal,
193
+ })
194
+
195
+ if (!response.ok || !response.body) {
196
+ postError(...)
197
+ return
198
+ }
199
+
200
+ postOpen(...)
201
+ await forwardSse(response.body, parsed => postEvent(...parsed))
202
+ postEnd(...)
203
+ } catch (err) {
204
+ if (err?.name !== 'AbortError') postError(...)
205
+ } finally {
206
+ activeStreams.delete(msg.id)
207
+ }
208
+ }
209
+ })
210
+ ```
211
+
212
+ ## Exact protocol summary
213
+
214
+ ### Child → parent
215
+
216
+ Standard stream request:
217
+
218
+ ```ts
219
+ {
220
+ _smartlinksProxyStreamRequest: true,
221
+ id,
222
+ method,
223
+ path,
224
+ body,
225
+ headers
226
+ }
227
+ ```
228
+
229
+ Abort request:
230
+
231
+ ```ts
232
+ {
233
+ _smartlinksProxyStreamAbort: true,
234
+ id
235
+ }
236
+ ```
237
+
238
+ ### Parent → child
239
+
240
+ Open:
241
+
242
+ ```ts
243
+ {
244
+ _smartlinksProxyStream: true,
245
+ id,
246
+ phase: 'open'
247
+ }
248
+ ```
249
+
250
+ Event:
251
+
252
+ ```ts
253
+ {
254
+ _smartlinksProxyStream: true,
255
+ id,
256
+ phase: 'event',
257
+ data: parsedJsonEvent
258
+ }
259
+ ```
260
+
261
+ End:
262
+
263
+ ```ts
264
+ {
265
+ _smartlinksProxyStream: true,
266
+ id,
267
+ phase: 'end'
268
+ }
269
+ ```
270
+
271
+ Error:
272
+
273
+ ```ts
274
+ {
275
+ _smartlinksProxyStream: true,
276
+ id,
277
+ phase: 'error',
278
+ error: 'message',
279
+ status?: number
280
+ }
281
+ ```
282
+
283
+ ## What does not change
284
+
285
+ These parts of the parent iframe integration stay the same:
286
+
287
+ - normal `_smartlinksProxyRequest` request/response flow
288
+ - upload proxy flow
289
+ - auth login/logout postMessage handling
290
+ - route/deep-link handling
291
+ - resize handling
292
+
293
+ This is an additive protocol, not a replacement.
294
+
295
+ ## Current SDK reference
296
+
297
+ The SDK implementation lives in:
298
+
299
+ - [src/http.ts](src/http.ts)
300
+ - [src/iframeResponder.ts](src/iframeResponder.ts)
301
+ - [src/types/iframeResponder.ts](src/types/iframeResponder.ts)
302
+ - [src/api/ai.ts](src/api/ai.ts)
303
+
304
+ ## Practical recommendation
305
+
306
+ If your parent already uses `IframeResponder`, prefer upgrading to the SDK version with these changes instead of re-implementing the protocol manually.
307
+
308
+ If your parent has a custom iframe bridge, implement exactly the three new message types above and reuse your existing auth/header logic from normal proxied requests.
package/openapi.yaml CHANGED
@@ -10782,7 +10782,7 @@ components:
10782
10782
  type: number
10783
10783
  area:
10784
10784
  type: number
10785
- AnalyticsStandardMetadataFields:
10785
+ AnalyticsStandardEventFields:
10786
10786
  type: object
10787
10787
  properties:
10788
10788
  visitorId:
@@ -10834,7 +10834,7 @@ components:
10834
10834
  type: object
10835
10835
  properties:
10836
10836
  sessionId:
10837
- type: string
10837
+ $ref: "#/components/schemas/AnalyticsSessionId"
10838
10838
  eventType:
10839
10839
  $ref: "#/components/schemas/AnalyticsEventType"
10840
10840
  collectionId:
@@ -10873,7 +10873,7 @@ components:
10873
10873
  type: object
10874
10874
  properties:
10875
10875
  sessionId:
10876
- type: string
10876
+ $ref: "#/components/schemas/AnalyticsSessionId"
10877
10877
  eventType:
10878
10878
  $ref: "#/components/schemas/AnalyticsEventType"
10879
10879
  collectionId:
@@ -11048,11 +11048,11 @@ components:
11048
11048
  items:
11049
11049
  type: string
11050
11050
  sessionId:
11051
- type: string
11051
+ $ref: "#/components/schemas/AnalyticsSessionId"
11052
11052
  sessionIds:
11053
11053
  type: array
11054
11054
  items:
11055
- type: string
11055
+ $ref: "#/components/schemas/AnalyticsSessionId"
11056
11056
  country:
11057
11057
  type: string
11058
11058
  countries:
@@ -15478,6 +15478,69 @@ components:
15478
15478
  required:
15479
15479
  - _smartlinksProxyResponse
15480
15480
  - id
15481
+ ProxyStreamRequest:
15482
+ type: object
15483
+ properties:
15484
+ _smartlinksProxyStreamRequest:
15485
+ type: object
15486
+ additionalProperties: true
15487
+ id:
15488
+ type: string
15489
+ method:
15490
+ type: string
15491
+ enum:
15492
+ - GET
15493
+ - POST
15494
+ - PUT
15495
+ - PATCH
15496
+ - DELETE
15497
+ path:
15498
+ type: string
15499
+ body: {}
15500
+ headers:
15501
+ type: object
15502
+ additionalProperties:
15503
+ type: string
15504
+ required:
15505
+ - _smartlinksProxyStreamRequest
15506
+ - id
15507
+ - method
15508
+ - path
15509
+ ProxyStreamAbortMessage:
15510
+ type: object
15511
+ properties:
15512
+ _smartlinksProxyStreamAbort:
15513
+ type: object
15514
+ additionalProperties: true
15515
+ id:
15516
+ type: string
15517
+ required:
15518
+ - _smartlinksProxyStreamAbort
15519
+ - id
15520
+ ProxyStreamMessage:
15521
+ type: object
15522
+ properties:
15523
+ _smartlinksProxyStream:
15524
+ type: object
15525
+ additionalProperties: true
15526
+ id:
15527
+ type: string
15528
+ phase:
15529
+ type: string
15530
+ enum:
15531
+ - open
15532
+ - event
15533
+ - end
15534
+ - error
15535
+ data: {}
15536
+ error:
15537
+ type: string
15538
+ status:
15539
+ type: number
15540
+ required:
15541
+ - _smartlinksProxyStream
15542
+ - id
15543
+ - phase
15481
15544
  UploadStartMessage:
15482
15545
  type: object
15483
15546
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",