@drivemetadata-ai/sdk 0.1.1-beta.4 → 0.1.1

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/README.md CHANGED
@@ -8,16 +8,50 @@ Enterprise-grade analytics, behavior intelligence, and customer data collection
8
8
  npm install @drivemetadata-ai/sdk
9
9
  ```
10
10
 
11
- ## Browser
11
+ For private beta installs, pin the beta tag:
12
+
13
+ ```bash
14
+ npm install @drivemetadata-ai/sdk@beta
15
+ ```
16
+
17
+ ## Runtime Entries
18
+
19
+ | Runtime | Import path | Use for |
20
+ |---|---|---|
21
+ | Browser | `@drivemetadata-ai/sdk/browser` | Plain JavaScript and browser applications |
22
+ | React | `@drivemetadata-ai/sdk/react` | React provider and hooks |
23
+ | Next.js | `@drivemetadata-ai/sdk/next` | App Router and Pages Router client helpers |
24
+ | Angular | `@drivemetadata-ai/sdk/angular` | Angular provider and injectable service |
25
+ | Node.js | `@drivemetadata-ai/sdk/node` | Backend/server events only |
26
+
27
+ Do not import `@drivemetadata-ai/sdk/node` from frontend bundles. Browser, React, Next.js, and Angular integrations use public browser tokens. Node integrations use server tokens from backend-only environment variables.
28
+
29
+ ## Required Browser Config
30
+
31
+ Use camelCase config keys in npm integrations:
12
32
 
13
33
  ```ts
14
- import { initDmdSDK } from '@drivemetadata-ai/sdk/browser';
34
+ const config = {
35
+ clientId: 'client_xxx',
36
+ workspaceId: 'workspace_xxx',
37
+ appId: 'app_xxx',
38
+ token: 'public_browser_token',
39
+ consent: { analytics: 'pending', advertising: 'denied' }
40
+ };
41
+ ```
42
+
43
+ `token` is the public browser write key. It is sent to the backend as `metaData.token`.
44
+
45
+ ## Browser Quick Start
15
46
 
16
- const dmd = initDmdSDK({
47
+ ```ts
48
+ import { consent, flush, getDmdHealth, identify, initDmdSDK, page, track } from '@drivemetadata-ai/sdk/browser';
49
+
50
+ initDmdSDK({
17
51
  clientId: 'client_xxx',
18
52
  workspaceId: 'workspace_xxx',
19
53
  appId: 'app_xxx',
20
- token: 'public_token',
54
+ token: 'public_browser_token',
21
55
  consent: {
22
56
  analytics: 'pending',
23
57
  advertising: 'denied',
@@ -27,26 +61,50 @@ const dmd = initDmdSDK({
27
61
  }
28
62
  });
29
63
 
30
- dmd.setConsent({ analytics: 'granted' });
31
- dmd.track('Product Viewed', {
32
- productId: 'sku_123'
33
- });
34
- await dmd.flush();
64
+ consent.update({ analytics: 'granted' });
65
+
66
+ identify('user_123', { plan: 'enterprise' });
67
+ page('Product Page');
68
+ track('Product Viewed', { productId: 'sku_123' });
69
+
70
+ await flush();
71
+ console.log(getDmdHealth());
35
72
  ```
36
73
 
74
+ Browser events automatically include backend `metaData` fields such as `requestId`, `anonymousId`, `sessionId`, timestamps, attribution/UTM enrichment, and page context.
75
+
37
76
  ## React
38
77
 
39
78
  ```tsx
40
- import { DmdProvider, useTrackEvent } from '@drivemetadata-ai/sdk/react';
79
+ import { DmdProvider, useDmdConsent, useDmdFlush, useIdentify, useTrackEvent } from '@drivemetadata-ai/sdk/react';
41
80
 
42
81
  function ProductButton() {
43
82
  const track = useTrackEvent();
44
- return <button onClick={() => track('Product Clicked')}>Track</button>;
83
+ const identify = useIdentify();
84
+ const flush = useDmdFlush();
85
+ const consent = useDmdConsent();
86
+
87
+ async function onClick() {
88
+ consent({ analytics: 'granted' });
89
+ identify('user_123', { plan: 'enterprise' });
90
+ track('Product Clicked', { productId: 'sku_123' });
91
+ await flush();
92
+ }
93
+
94
+ return <button onClick={onClick}>Track</button>;
45
95
  }
46
96
 
47
97
  export function App() {
48
98
  return (
49
- <DmdProvider config={config}>
99
+ <DmdProvider
100
+ config={{
101
+ clientId: 'client_xxx',
102
+ workspaceId: 'workspace_xxx',
103
+ appId: 'app_xxx',
104
+ token: 'public_browser_token',
105
+ consent: { analytics: 'pending', advertising: 'denied' }
106
+ }}
107
+ >
50
108
  <ProductButton />
51
109
  </DmdProvider>
52
110
  );
@@ -60,15 +118,55 @@ Use `@drivemetadata-ai/sdk/next` from a client component.
60
118
  ```tsx
61
119
  'use client';
62
120
 
63
- import { DmdProvider } from '@drivemetadata-ai/sdk/next';
121
+ import { DmdProvider, useDmdAppRouterPageTracking } from '@drivemetadata-ai/sdk/next';
122
+ import { usePathname, useSearchParams } from 'next/navigation';
123
+ import type { ReactNode } from 'react';
124
+
125
+ function RouteTracking() {
126
+ const pathname = usePathname();
127
+ const searchParams = useSearchParams();
128
+ useDmdAppRouterPageTracking(pathname, searchParams.toString());
129
+ return null;
130
+ }
131
+
132
+ export function AnalyticsProvider({ children }: { children: ReactNode }) {
133
+ return (
134
+ <DmdProvider
135
+ config={{
136
+ clientId: process.env.NEXT_PUBLIC_DMD_CLIENT_ID!,
137
+ workspaceId: process.env.NEXT_PUBLIC_DMD_WORKSPACE_ID!,
138
+ appId: process.env.NEXT_PUBLIC_DMD_APP_ID!,
139
+ token: process.env.NEXT_PUBLIC_DMD_TOKEN!,
140
+ consent: { analytics: 'pending', advertising: 'denied' }
141
+ }}
142
+ >
143
+ <RouteTracking />
144
+ {children}
145
+ </DmdProvider>
146
+ );
147
+ }
64
148
  ```
65
149
 
66
150
  ## Angular
67
151
 
68
152
  ```ts
69
153
  import { DmdAnalyticsService, provideDmdAnalytics } from '@drivemetadata-ai/sdk/angular';
154
+
155
+ export const appConfig = {
156
+ providers: [
157
+ provideDmdAnalytics({
158
+ clientId: 'client_xxx',
159
+ workspaceId: 'workspace_xxx',
160
+ appId: 'app_xxx',
161
+ token: 'public_browser_token',
162
+ consent: { analytics: 'pending', advertising: 'denied' }
163
+ })
164
+ ]
165
+ };
70
166
  ```
71
167
 
168
+ Call `DmdAnalyticsService.init()` only in the browser when using Angular Universal/SSR.
169
+
72
170
  ## Node.js
73
171
 
74
172
  ```ts
@@ -78,23 +176,56 @@ const dmd = createDmdServerClient({
78
176
  clientId: 'client_xxx',
79
177
  workspaceId: 'workspace_xxx',
80
178
  appId: 'app_xxx',
81
- token: process.env.DMD_SERVER_TOKEN!
179
+ token: process.env.DMD_SERVER_TOKEN!,
180
+ timeoutMs: 5000,
181
+ retry: {
182
+ attempts: 3,
183
+ minDelayMs: 250,
184
+ maxDelayMs: 2000
185
+ }
82
186
  });
83
187
 
84
188
  await dmd.track({
85
189
  userId: 'user_123',
86
190
  event: 'Order Completed',
87
- idempotencyKey: 'order_123'
191
+ idempotencyKey: 'order_123',
192
+ properties: {
193
+ amount: 500,
194
+ currency: 'USD'
195
+ }
88
196
  });
89
197
  ```
90
198
 
91
- ## Runtime Entries
199
+ ## Default Page Context
200
+
201
+ Browser, React, Next.js, and Angular integrations automatically add page context to each collector payload:
202
+
203
+ ```json
204
+ {
205
+ "metaData": {
206
+ "page": {
207
+ "url": "https://example.com/products/sku-123?utm_source=paid",
208
+ "path": "/products/sku-123",
209
+ "search": "?utm_source=paid",
210
+ "title": "Product Detail",
211
+ "referrer": "https://google.com/search?q=%5BREDACTED%5D"
212
+ }
213
+ }
214
+ }
215
+ ```
216
+
217
+ Query strings in `search` and `referrer` are redacted by default except common attribution parameters.
218
+
219
+ ## Diagnostics
220
+
221
+ ```ts
222
+ import { flush, getDmdHealth } from '@drivemetadata-ai/sdk/browser';
223
+
224
+ console.log(getDmdHealth());
225
+ await flush();
226
+ ```
92
227
 
93
- - `@drivemetadata-ai/sdk/browser`
94
- - `@drivemetadata-ai/sdk/react`
95
- - `@drivemetadata-ai/sdk/next`
96
- - `@drivemetadata-ai/sdk/angular`
97
- - `@drivemetadata-ai/sdk/node`
228
+ Useful drop reasons include `consent_denied`, `payload_too_large`, `queue_limit_exceeded`, and `queue_ttl_expired`.
98
229
 
99
230
  ## Documentation
100
231
 
@@ -264,6 +264,15 @@ var sensitiveKeys = /* @__PURE__ */ new Set([
264
264
  "session",
265
265
  "cookie"
266
266
  ]);
267
+ function redactUrl(url, allowQueryKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]) {
268
+ const parsed = new URL(url, "https://placeholder.local");
269
+ for (const key of Array.from(parsed.searchParams.keys())) {
270
+ if (!allowQueryKeys.includes(key)) {
271
+ parsed.searchParams.set(key, "[REDACTED]");
272
+ }
273
+ }
274
+ return url.startsWith("http") ? parsed.toString() : `${parsed.pathname}${parsed.search}`;
275
+ }
267
276
  function sanitizeValue(value, allow, path = []) {
268
277
  if (Array.isArray(value)) {
269
278
  return value.map((item) => sanitizeValue(item, allow, path));
@@ -285,6 +294,127 @@ function sanitizeProperties(input, allowRawKeys = []) {
285
294
  return sanitizeValue(input, allow);
286
295
  }
287
296
 
297
+ // src/browser/core/attribute-persistence.ts
298
+ var DMD_ATTRIBUTES_STORAGE_KEY = "dmd_attributes";
299
+ var SESSION_STORAGE_KEY = "sessionData";
300
+ var SESSION_ID_KEY = "sessionId";
301
+ var SESSION_TIMESTAMP_KEY = "timestamp";
302
+ function isBlankValue(value) {
303
+ return value === null || value === void 0 || typeof value === "string" && value.trim() === "";
304
+ }
305
+ function readAttributes(base) {
306
+ const raw = base.getItem(DMD_ATTRIBUTES_STORAGE_KEY);
307
+ if (!raw) return {};
308
+ try {
309
+ const parsed = JSON.parse(raw);
310
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
311
+ } catch {
312
+ base.removeItem(DMD_ATTRIBUTES_STORAGE_KEY);
313
+ return {};
314
+ }
315
+ }
316
+ function sanitizeAttributes(attributes) {
317
+ return Object.fromEntries(Object.entries(attributes).filter(([, value]) => !isBlankValue(value)));
318
+ }
319
+ function writeAttributes(base, attributes) {
320
+ const sanitized = sanitizeAttributes(attributes);
321
+ if (Object.keys(sanitized).length === 0) {
322
+ base.removeItem(DMD_ATTRIBUTES_STORAGE_KEY);
323
+ return;
324
+ }
325
+ base.setItem(DMD_ATTRIBUTES_STORAGE_KEY, JSON.stringify(sanitized));
326
+ }
327
+ function parseSessionData(value) {
328
+ try {
329
+ const parsed = JSON.parse(value);
330
+ const sessionData = {};
331
+ const sessionId = typeof parsed.sessionId === "string" && parsed.sessionId.trim() !== "" ? parsed.sessionId : void 0;
332
+ const timestamp = typeof parsed.timestamp === "number" ? parsed.timestamp : void 0;
333
+ if (sessionId !== void 0) sessionData.sessionId = sessionId;
334
+ if (timestamp !== void 0) sessionData.timestamp = timestamp;
335
+ return sessionData;
336
+ } catch {
337
+ return {};
338
+ }
339
+ }
340
+ function getAggregateValue(attributes, key) {
341
+ if (key === SESSION_STORAGE_KEY) {
342
+ const sessionId = attributes[SESSION_ID_KEY];
343
+ const timestamp = attributes[SESSION_TIMESTAMP_KEY];
344
+ if (typeof sessionId === "string" && !isBlankValue(sessionId) && typeof timestamp === "number") {
345
+ return JSON.stringify({ sessionId, timestamp });
346
+ }
347
+ return null;
348
+ }
349
+ const value = attributes[key];
350
+ if (isBlankValue(value)) return null;
351
+ return typeof value === "string" ? value : JSON.stringify(value);
352
+ }
353
+ function setAggregateValue(attributes, key, value) {
354
+ if (key === SESSION_STORAGE_KEY) {
355
+ const sessionData = parseSessionData(value);
356
+ if (sessionData.sessionId === void 0 || sessionData.timestamp === void 0) {
357
+ delete attributes[SESSION_ID_KEY];
358
+ delete attributes[SESSION_TIMESTAMP_KEY];
359
+ return;
360
+ }
361
+ attributes[SESSION_ID_KEY] = sessionData.sessionId;
362
+ attributes[SESSION_TIMESTAMP_KEY] = sessionData.timestamp;
363
+ return;
364
+ }
365
+ attributes[key] = value;
366
+ }
367
+ function removeAggregateValue(attributes, key) {
368
+ if (key === SESSION_STORAGE_KEY) {
369
+ delete attributes[SESSION_ID_KEY];
370
+ delete attributes[SESSION_TIMESTAMP_KEY];
371
+ return;
372
+ }
373
+ delete attributes[key];
374
+ }
375
+ function createDmdAttributesPersistence(base) {
376
+ return {
377
+ getItem(key) {
378
+ if (key === DMD_ATTRIBUTES_STORAGE_KEY) return base.getItem(key);
379
+ const attributes = readAttributes(base);
380
+ const aggregateValue = getAggregateValue(attributes, key);
381
+ if (aggregateValue !== null) return aggregateValue;
382
+ const legacyValue = base.getItem(key);
383
+ if (isBlankValue(legacyValue)) return null;
384
+ setAggregateValue(attributes, key, String(legacyValue));
385
+ writeAttributes(base, attributes);
386
+ return legacyValue;
387
+ },
388
+ setItem(key, value) {
389
+ if (key === DMD_ATTRIBUTES_STORAGE_KEY) return base.setItem(key, value);
390
+ const attributes = readAttributes(base);
391
+ if (isBlankValue(value)) {
392
+ removeAggregateValue(attributes, key);
393
+ writeAttributes(base, attributes);
394
+ base.removeItem(key);
395
+ return true;
396
+ }
397
+ setAggregateValue(attributes, key, value);
398
+ writeAttributes(base, attributes);
399
+ base.setItem(key, value);
400
+ return true;
401
+ },
402
+ removeItem(key) {
403
+ if (key === DMD_ATTRIBUTES_STORAGE_KEY) {
404
+ base.removeItem(key);
405
+ return;
406
+ }
407
+ const attributes = readAttributes(base);
408
+ removeAggregateValue(attributes, key);
409
+ writeAttributes(base, attributes);
410
+ base.removeItem(key);
411
+ },
412
+ getHealth() {
413
+ return base.getHealth();
414
+ }
415
+ };
416
+ }
417
+
288
418
  // src/core/attribution.ts
289
419
  var dmdUtmKeys = [
290
420
  "utm_source",
@@ -321,6 +451,10 @@ function mergeAttributionRecords(explicitProperties, stored) {
321
451
  }
322
452
 
323
453
  // src/browser/core/attribution.ts
454
+ var DMD_CLICK_ID_QUERY_KEY = "click_id";
455
+ var DMD_CLICK_ID_LEGACY_QUERY_KEY = "dmd_click_id";
456
+ var DMD_CLICK_ID_STORAGE_KEY = "dmd_click_id";
457
+ var DMD_CLICK_ID_ATTRIBUTE_KEY = "click_id";
324
458
  function getCurrentUrl() {
325
459
  return getBrowserWindow()?.location?.href;
326
460
  }
@@ -339,6 +473,9 @@ function setIfPresent(persistence, key, value) {
339
473
  persistence.setItem(key, value);
340
474
  }
341
475
  }
476
+ function getClickIdParam(params) {
477
+ return params.get(DMD_CLICK_ID_LEGACY_QUERY_KEY) || params.get(DMD_CLICK_ID_QUERY_KEY);
478
+ }
342
479
  function getStoredString(persistence, key) {
343
480
  const value = persistence.getItem(key);
344
481
  return value === null || value === "" ? void 0 : value;
@@ -358,6 +495,9 @@ function captureAttributionFromUrl(persistence, url = getCurrentUrl()) {
358
495
  for (const key of dmdCampaignKeys) {
359
496
  setIfPresent(persistence, key, params.get(key));
360
497
  }
498
+ const clickId = getClickIdParam(params);
499
+ setIfPresent(persistence, DMD_CLICK_ID_ATTRIBUTE_KEY, clickId);
500
+ setIfPresent(persistence, DMD_CLICK_ID_STORAGE_KEY, clickId);
361
501
  for (const [param, sdkPubId] of dmdClickIdSources) {
362
502
  const value = params.get(param);
363
503
  if (value) {
@@ -373,7 +513,8 @@ function getStoredAttributionData(persistence) {
373
513
  fbc: getStoredString(persistence, "fbc") ?? getCookie("_fbc"),
374
514
  fbp: getStoredString(persistence, "fbp") ?? getCookie("_fbp"),
375
515
  campaign_id: getStoredString(persistence, "campaign_id"),
376
- ad_id: getStoredString(persistence, "ad_id")
516
+ ad_id: getStoredString(persistence, "ad_id"),
517
+ click_id: getStoredString(persistence, DMD_CLICK_ID_STORAGE_KEY) ?? getStoredString(persistence, DMD_CLICK_ID_ATTRIBUTE_KEY)
377
518
  });
378
519
  }
379
520
  function getStoredUtmParameter(persistence) {
@@ -892,6 +1033,24 @@ function resetAnonymousId(persistence) {
892
1033
  return anonymousId;
893
1034
  }
894
1035
 
1036
+ // src/browser/core/page-context.ts
1037
+ function redactSearch(search) {
1038
+ const redacted = redactUrl(search);
1039
+ return redacted.startsWith("/?") ? redacted.slice(1) : redacted;
1040
+ }
1041
+ function getBrowserPageContext() {
1042
+ const browserWindow = getBrowserWindow();
1043
+ const documentRef = browserWindow?.document ?? globalThis.document;
1044
+ const location = browserWindow?.location;
1045
+ const page2 = {};
1046
+ if (location?.href) page2.url = location.href;
1047
+ if (location?.pathname) page2.path = location.pathname;
1048
+ if (location?.search) page2.search = redactSearch(location.search);
1049
+ if (documentRef?.title) page2.title = documentRef.title;
1050
+ if (documentRef?.referrer) page2.referrer = redactUrl(documentRef.referrer);
1051
+ return page2;
1052
+ }
1053
+
895
1054
  // src/browser/core/persistence.ts
896
1055
  function createPersistenceHealth(requested) {
897
1056
  return {
@@ -1058,7 +1217,7 @@ function createBrowserPersistence(mode = "localStorage+cookie") {
1058
1217
  }
1059
1218
 
1060
1219
  // src/browser/core/session.ts
1061
- var SESSION_STORAGE_KEY = "sessionData";
1220
+ var SESSION_STORAGE_KEY2 = "sessionData";
1062
1221
  function getUrlParam2(name) {
1063
1222
  const href = getBrowserWindow()?.location?.href;
1064
1223
  if (!href) return null;
@@ -1069,7 +1228,7 @@ function getUrlParam2(name) {
1069
1228
  }
1070
1229
  }
1071
1230
  function readStoredSession(persistence) {
1072
- const rawSession = persistence.getItem(SESSION_STORAGE_KEY);
1231
+ const rawSession = persistence.getItem(SESSION_STORAGE_KEY2);
1073
1232
  if (!rawSession) return void 0;
1074
1233
  try {
1075
1234
  const parsed = JSON.parse(rawSession);
@@ -1080,12 +1239,12 @@ function readStoredSession(persistence) {
1080
1239
  };
1081
1240
  }
1082
1241
  } catch {
1083
- persistence.removeItem(SESSION_STORAGE_KEY);
1242
+ persistence.removeItem(SESSION_STORAGE_KEY2);
1084
1243
  }
1085
1244
  return void 0;
1086
1245
  }
1087
1246
  function writeStoredSession(persistence, sessionId, timestamp) {
1088
- persistence.setItem(SESSION_STORAGE_KEY, JSON.stringify({ sessionId, timestamp }));
1247
+ persistence.setItem(SESSION_STORAGE_KEY2, JSON.stringify({ sessionId, timestamp }));
1089
1248
  }
1090
1249
  function createSessionManager(persistence, idleTimeoutSeconds = 30 * 60) {
1091
1250
  function resolveSessionId() {
@@ -1139,11 +1298,12 @@ var DriveMetaDataSDK = class {
1139
1298
  requireConfigString(config.writeKey || config.token, "writeKey or token");
1140
1299
  this.config = config;
1141
1300
  this.endpoint = endpointFromConfig(config);
1142
- this.persistence = createBrowserPersistence(config.persistence);
1301
+ const storagePersistence = createBrowserPersistence(config.persistence);
1302
+ this.persistence = createDmdAttributesPersistence(storagePersistence);
1143
1303
  this.session = createSessionManager(this.persistence, config.sessionIdleTimeoutSeconds);
1144
1304
  const deliveryConfig = {
1145
1305
  endpoint: this.endpoint,
1146
- storage: this.persistence
1306
+ storage: storagePersistence
1147
1307
  };
1148
1308
  if (config.delivery?.maxQueueSize !== void 0) deliveryConfig.maxQueueSize = config.delivery.maxQueueSize;
1149
1309
  if (config.delivery?.queueTtlMs !== void 0) deliveryConfig.queueTtlMs = config.delivery.queueTtlMs;
@@ -1388,6 +1548,7 @@ var DriveMetaDataSDK = class {
1388
1548
  }
1389
1549
  toCollectorPayload(prepared) {
1390
1550
  const browserWindow = getBrowserWindow();
1551
+ const pageContext = getBrowserPageContext();
1391
1552
  return createBackendCollectorPayload({
1392
1553
  requestId: prepared.messageId,
1393
1554
  timestamp: prepared.timestamp,
@@ -1399,9 +1560,13 @@ var DriveMetaDataSDK = class {
1399
1560
  sessionId: prepared.sessionId,
1400
1561
  ua: browserWindow?.navigator?.userAgent,
1401
1562
  appId: prepared.appId,
1402
- pageUrl: browserWindow?.location?.href,
1563
+ pageUrl: pageContext.url ?? browserWindow?.location?.href,
1403
1564
  eventData: mergeStoredAttribution({
1404
1565
  ...prepared.properties,
1566
+ page: {
1567
+ ...pageContext,
1568
+ ...prepared.properties.page && typeof prepared.properties.page === "object" ? prepared.properties.page : {}
1569
+ },
1405
1570
  anonymousId: prepared.anonymousId,
1406
1571
  sessionId: prepared.sessionId,
1407
1572
  timestamp: prepared.timestamp,