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

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.
@@ -1,12 +1,62 @@
1
1
  // src/react/DmdProvider.tsx
2
2
  import React from "react";
3
3
 
4
+ // src/browser/core/browser-config.ts
5
+ function setIfDefined(target, key, value) {
6
+ if (value !== void 0) {
7
+ target[key] = value;
8
+ }
9
+ }
10
+ function normalizeBrowserConfig(input) {
11
+ const normalized = {
12
+ ...input,
13
+ clientId: input.clientId ?? "",
14
+ workspaceId: input.workspaceId ?? "",
15
+ appId: input.appId ?? ""
16
+ };
17
+ setIfDefined(normalized, "apiHost", input.apiHost);
18
+ setIfDefined(normalized, "capturePageview", input.capturePageview);
19
+ setIfDefined(normalized, "capturePageleave", input.capturePageleave);
20
+ setIfDefined(normalized, "captureDeadClicks", input.captureDeadClicks);
21
+ setIfDefined(normalized, "crossSubdomainCookie", input.crossSubdomainCookie);
22
+ setIfDefined(normalized, "sessionIdleTimeoutSeconds", input.sessionIdleTimeoutSeconds);
23
+ setIfDefined(normalized, "schemaValidation", input.schemaValidation);
24
+ setIfDefined(normalized, "beforeSend", input.beforeSend);
25
+ setIfDefined(normalized, "persistence", input.disablePersistence === true ? "none" : input.persistence);
26
+ return normalized;
27
+ }
28
+
29
+ // src/core/uuid.ts
30
+ function createUuid() {
31
+ const cryptoApi = globalThis.crypto;
32
+ if (typeof cryptoApi?.randomUUID === "function") {
33
+ return cryptoApi.randomUUID();
34
+ }
35
+ const bytes = new Uint8Array(16);
36
+ if (typeof cryptoApi?.getRandomValues === "function") {
37
+ cryptoApi.getRandomValues(bytes);
38
+ } else {
39
+ for (let index = 0; index < bytes.length; index += 1) {
40
+ bytes[index] = Math.floor(Math.random() * 256);
41
+ }
42
+ }
43
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
44
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
45
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
46
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
47
+ }
48
+ function isUuid(value) {
49
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
50
+ }
51
+ function ensureUuid(value) {
52
+ return isUuid(value) ? value : createUuid();
53
+ }
54
+
4
55
  // src/core/backend-payload.ts
5
56
  function formatUtcTimestamp(value) {
6
- if (typeof value !== "string") return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
7
- const date = new Date(value);
8
- if (Number.isNaN(date.getTime())) return value;
9
- return date.toISOString().replace("T", " ").slice(0, 19);
57
+ const date = typeof value === "string" || typeof value === "number" || value instanceof Date ? new Date(value) : /* @__PURE__ */ new Date();
58
+ const safeDate = Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
59
+ return safeDate.toISOString().replace("T", " ").slice(0, 19);
10
60
  }
11
61
  function cleanObject(value) {
12
62
  if (Array.isArray(value)) {
@@ -37,15 +87,19 @@ function createBackendCollectorPayload(input) {
37
87
  };
38
88
  const metaData = {
39
89
  ...normalizedEventData,
40
- requestId: input.requestId,
90
+ requestId: ensureUuid(typeof input.requestId === "string" ? input.requestId : void 0),
41
91
  timestamp,
42
92
  eventType: input.eventType,
43
93
  requestFrom: input.requestFrom ?? "3",
44
94
  clientId: input.clientId,
45
95
  workspaceId: input.workspaceId,
46
96
  token: input.token,
47
- anonymousId: eventData.anonymousId ?? input.anonymousId,
48
- sessionId: eventData.sessionId ?? input.sessionId,
97
+ anonymousId: ensureUuid(
98
+ typeof eventData.anonymousId === "string" ? eventData.anonymousId : typeof input.anonymousId === "string" ? input.anonymousId : void 0
99
+ ),
100
+ sessionId: ensureUuid(
101
+ typeof eventData.sessionId === "string" ? eventData.sessionId : typeof input.sessionId === "string" ? input.sessionId : void 0
102
+ ),
49
103
  ua: input.ua,
50
104
  appDetails: { app_id: input.appId },
51
105
  page: { ...page2, url: page2.url ?? input.pageUrl },
@@ -56,12 +110,45 @@ function createBackendCollectorPayload(input) {
56
110
  return cleanObject(payload);
57
111
  }
58
112
 
59
- // src/core/environment.ts
60
- function getBrowserWindow() {
61
- return typeof window === "undefined" ? void 0 : window;
113
+ // src/core/backend-schema.ts
114
+ var requiredMetaDataFields = [
115
+ "requestId",
116
+ "timestamp",
117
+ "eventType",
118
+ "requestFrom",
119
+ "clientId",
120
+ "workspaceId",
121
+ "anonymousId",
122
+ "sessionId"
123
+ ];
124
+ function isPresent(value) {
125
+ return value !== null && value !== void 0 && value !== "";
126
+ }
127
+ function validateBackendCollectorPayload(payload) {
128
+ const errors = [];
129
+ const metaData = payload.metaData;
130
+ if (!metaData || typeof metaData !== "object" || Array.isArray(metaData)) {
131
+ return {
132
+ ok: false,
133
+ errors: ["metaData is required"]
134
+ };
135
+ }
136
+ const metadataRecord = metaData;
137
+ for (const field of requiredMetaDataFields) {
138
+ if (!isPresent(metadataRecord[field])) {
139
+ errors.push(`metaData.${field} is required`);
140
+ }
141
+ }
142
+ if (isPresent(metadataRecord.eventType) && typeof metadataRecord.eventType !== "string") {
143
+ errors.push("metaData.eventType must be a string");
144
+ }
145
+ return {
146
+ ok: errors.length === 0,
147
+ errors
148
+ };
62
149
  }
63
150
 
64
- // src/browser/core/consent.ts
151
+ // src/core/consent.ts
65
152
  var purposes = [
66
153
  "analytics",
67
154
  "advertising",
@@ -101,6 +188,335 @@ function canCollectPurpose(consent, purpose) {
101
188
  return consent[purpose] === "granted";
102
189
  }
103
190
 
191
+ // src/core/event-schema.ts
192
+ var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
193
+ function validateEventEnvelope(envelope, options = {}) {
194
+ if (options.mode === "off") return { ok: true, errors: [] };
195
+ const errors = [];
196
+ if (typeof envelope.type !== "string") errors.push("type must be a string");
197
+ if (envelope.type === "track" && typeof envelope.event !== "string") {
198
+ errors.push("track event must include event name");
199
+ }
200
+ if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
201
+ if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
202
+ for (const key of Object.keys(envelope.properties ?? {})) {
203
+ if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
204
+ }
205
+ return { ok: errors.length === 0, errors };
206
+ }
207
+
208
+ // src/core/environment.ts
209
+ function getBrowserWindow() {
210
+ return typeof window === "undefined" ? void 0 : window;
211
+ }
212
+
213
+ // src/core/privacy.ts
214
+ var sensitiveKeys = /* @__PURE__ */ new Set([
215
+ "email",
216
+ "phone",
217
+ "mobile",
218
+ "address",
219
+ "address1",
220
+ "address2",
221
+ "first_name",
222
+ "last_name",
223
+ "name",
224
+ "token",
225
+ "secret",
226
+ "password",
227
+ "session",
228
+ "cookie"
229
+ ]);
230
+ function sanitizeValue(value, allow, path = []) {
231
+ if (Array.isArray(value)) {
232
+ return value.map((item) => sanitizeValue(item, allow, path));
233
+ }
234
+ if (value && typeof value === "object") {
235
+ return Object.fromEntries(
236
+ Object.entries(value).filter(([key]) => {
237
+ const lowerKey = key.toLowerCase();
238
+ const lowerPath = path.map((item) => item.toLowerCase());
239
+ const isEcommerceItemName = lowerKey === "name" && lowerPath.includes("ecommerce") && lowerPath.includes("items");
240
+ return isEcommerceItemName || !sensitiveKeys.has(lowerKey) || allow.has(lowerKey);
241
+ }).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow, [...path, key])])
242
+ );
243
+ }
244
+ return value;
245
+ }
246
+ function sanitizeProperties(input, allowRawKeys = []) {
247
+ const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
248
+ return sanitizeValue(input, allow);
249
+ }
250
+
251
+ // src/core/attribution.ts
252
+ var dmdUtmKeys = [
253
+ "utm_source",
254
+ "utm_medium",
255
+ "utm_campaign",
256
+ "utm_term",
257
+ "utm_content",
258
+ "utm_id"
259
+ ];
260
+ var dmdCampaignKeys = ["campaign_id", "ad_id"];
261
+ var dmdClickIdSources = [
262
+ ["gclid", 2],
263
+ ["fbclid", 3],
264
+ ["ScCid", 1],
265
+ ["li_fat_id", 4]
266
+ ];
267
+ function cleanAttributionRecord(record) {
268
+ const cleaned = Object.fromEntries(
269
+ Object.entries(record).filter(([, value]) => value !== null && value !== void 0 && value !== "")
270
+ );
271
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
272
+ }
273
+ function objectValue(value) {
274
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
275
+ }
276
+ function mergeAttributionRecords(explicitProperties, stored) {
277
+ const attributionData = objectValue(explicitProperties.attributionData);
278
+ const utmParameter = objectValue(explicitProperties.utmParameter);
279
+ return {
280
+ ...explicitProperties,
281
+ ...stored.attributionData || attributionData ? { attributionData: { ...stored.attributionData ?? {}, ...attributionData ?? {} } } : {},
282
+ ...stored.utmParameter || utmParameter ? { utmParameter: { ...stored.utmParameter ?? {}, ...utmParameter ?? {} } } : {}
283
+ };
284
+ }
285
+
286
+ // src/browser/core/attribution.ts
287
+ function getCurrentUrl() {
288
+ return getBrowserWindow()?.location?.href;
289
+ }
290
+ function getCookie(name) {
291
+ const cookie = getBrowserWindow()?.document?.cookie;
292
+ if (!cookie) return void 0;
293
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
294
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
295
+ return match ? decodeURIComponent(match[1] ?? "") : void 0;
296
+ }
297
+ function getParams(url) {
298
+ return new URL(url, "https://placeholder.local").searchParams;
299
+ }
300
+ function setIfPresent(persistence, key, value) {
301
+ if (value !== null && value !== "") {
302
+ persistence.setItem(key, value);
303
+ }
304
+ }
305
+ function getStoredString(persistence, key) {
306
+ const value = persistence.getItem(key);
307
+ return value === null || value === "" ? void 0 : value;
308
+ }
309
+ function getStoredNumberOrString(persistence, key) {
310
+ const value = getStoredString(persistence, key);
311
+ if (value === void 0) return void 0;
312
+ const parsed = Number.parseInt(value, 10);
313
+ return Number.isNaN(parsed) ? value : parsed;
314
+ }
315
+ function captureAttributionFromUrl(persistence, url = getCurrentUrl()) {
316
+ if (!url) return;
317
+ const params = getParams(url);
318
+ for (const key of dmdUtmKeys) {
319
+ setIfPresent(persistence, key, params.get(key));
320
+ }
321
+ for (const key of dmdCampaignKeys) {
322
+ setIfPresent(persistence, key, params.get(key));
323
+ }
324
+ for (const [param, sdkPubId] of dmdClickIdSources) {
325
+ const value = params.get(param);
326
+ if (value) {
327
+ persistence.setItem("unique_id", value);
328
+ persistence.setItem("sdk_pub_id", String(sdkPubId));
329
+ }
330
+ }
331
+ }
332
+ function getStoredAttributionData(persistence) {
333
+ return cleanAttributionRecord({
334
+ unique_id: getStoredString(persistence, "unique_id"),
335
+ sdk_pub_id: getStoredNumberOrString(persistence, "sdk_pub_id"),
336
+ fbc: getStoredString(persistence, "fbc") ?? getCookie("_fbc"),
337
+ fbp: getStoredString(persistence, "fbp") ?? getCookie("_fbp"),
338
+ campaign_id: getStoredString(persistence, "campaign_id"),
339
+ ad_id: getStoredString(persistence, "ad_id")
340
+ });
341
+ }
342
+ function getStoredUtmParameter(persistence) {
343
+ return cleanAttributionRecord(Object.fromEntries(dmdUtmKeys.map((key) => [key, getStoredString(persistence, key)])));
344
+ }
345
+ function mergeStoredAttribution(properties, persistence) {
346
+ const stored = {};
347
+ const attributionData = getStoredAttributionData(persistence);
348
+ const utmParameter = getStoredUtmParameter(persistence);
349
+ if (attributionData !== void 0) stored.attributionData = attributionData;
350
+ if (utmParameter !== void 0) stored.utmParameter = utmParameter;
351
+ return mergeAttributionRecords(properties, stored);
352
+ }
353
+
354
+ // src/browser/core/autocapture.ts
355
+ function installAutocapture(config) {
356
+ const trackedPageUrls = /* @__PURE__ */ new Set();
357
+ const timers = /* @__PURE__ */ new Set();
358
+ const pageviewDelayMs = config.pageviewDelayMs ?? 500;
359
+ const history = config.browserWindow.history;
360
+ const originalPushState = history?.pushState;
361
+ const originalReplaceState = history?.replaceState;
362
+ function clearTimer(timer) {
363
+ timers.delete(timer);
364
+ clearTimeout(timer);
365
+ }
366
+ function canonicalUrl() {
367
+ return config.browserWindow.location.href;
368
+ }
369
+ function trackCurrentPageview() {
370
+ if (!config.capturePageview) return;
371
+ const url = canonicalUrl();
372
+ if (trackedPageUrls.has(url)) return;
373
+ trackedPageUrls.add(url);
374
+ config.onPageView();
375
+ }
376
+ function schedulePageview(delayMs) {
377
+ if (!config.capturePageview) return;
378
+ const timer = setTimeout(() => {
379
+ timers.delete(timer);
380
+ trackCurrentPageview();
381
+ }, delayMs);
382
+ timers.add(timer);
383
+ }
384
+ function handleRouteChange() {
385
+ config.onRouteChange?.();
386
+ trackCurrentPageview();
387
+ }
388
+ function wrapHistoryMethod(method) {
389
+ if (!history) return;
390
+ const original = history[method];
391
+ if (typeof original !== "function") return;
392
+ history[method] = function wrappedHistoryMethod(...args) {
393
+ const result = original.apply(this, args);
394
+ const timer = setTimeout(() => {
395
+ timers.delete(timer);
396
+ handleRouteChange();
397
+ }, 0);
398
+ timers.add(timer);
399
+ return result;
400
+ };
401
+ }
402
+ function handlePopOrHashChange() {
403
+ handleRouteChange();
404
+ }
405
+ function handlePageLeave() {
406
+ if (config.capturePageleave) {
407
+ config.onPageLeave();
408
+ }
409
+ }
410
+ schedulePageview(pageviewDelayMs);
411
+ wrapHistoryMethod("pushState");
412
+ wrapHistoryMethod("replaceState");
413
+ const canListen = typeof config.browserWindow.addEventListener === "function" && typeof config.browserWindow.removeEventListener === "function";
414
+ if (canListen) {
415
+ config.browserWindow.addEventListener("popstate", handlePopOrHashChange);
416
+ config.browserWindow.addEventListener("hashchange", handlePopOrHashChange);
417
+ config.browserWindow.addEventListener("beforeunload", handlePageLeave);
418
+ }
419
+ return {
420
+ cleanup() {
421
+ for (const timer of Array.from(timers)) {
422
+ clearTimer(timer);
423
+ }
424
+ if (history && originalPushState) history.pushState = originalPushState;
425
+ if (history && originalReplaceState) history.replaceState = originalReplaceState;
426
+ if (canListen) {
427
+ config.browserWindow.removeEventListener("popstate", handlePopOrHashChange);
428
+ config.browserWindow.removeEventListener("hashchange", handlePopOrHashChange);
429
+ config.browserWindow.removeEventListener("beforeunload", handlePageLeave);
430
+ }
431
+ }
432
+ };
433
+ }
434
+
435
+ // src/core/payload-size.ts
436
+ var preserveStringKeys = /* @__PURE__ */ new Set([
437
+ "requestId",
438
+ "eventType",
439
+ "requestFrom",
440
+ "clientId",
441
+ "workspaceId",
442
+ "anonymousId",
443
+ "sessionId",
444
+ "token"
445
+ ]);
446
+ function payloadByteLength(payload) {
447
+ const serialized = JSON.stringify(payload);
448
+ if (typeof TextEncoder !== "undefined") {
449
+ return new TextEncoder().encode(serialized).length;
450
+ }
451
+ return serialized.length;
452
+ }
453
+ function clonePayload(payload) {
454
+ try {
455
+ return JSON.parse(JSON.stringify(payload));
456
+ } catch {
457
+ return void 0;
458
+ }
459
+ }
460
+ function truncateValue(value, truncateStringLength, key) {
461
+ if (typeof value === "string") {
462
+ if (key && preserveStringKeys.has(key)) return value;
463
+ if (value.length <= truncateStringLength) return value;
464
+ return `${value.slice(0, truncateStringLength)}...[TRUNCATED]`;
465
+ }
466
+ if (Array.isArray(value)) {
467
+ return value.map((item) => truncateValue(item, truncateStringLength));
468
+ }
469
+ if (value && typeof value === "object") {
470
+ return Object.fromEntries(
471
+ Object.entries(value).map(([childKey, childValue]) => [
472
+ childKey,
473
+ truncateValue(childValue, truncateStringLength, childKey)
474
+ ])
475
+ );
476
+ }
477
+ return value;
478
+ }
479
+ function markPayloadTruncated(payload) {
480
+ if (payload.metaData && typeof payload.metaData === "object" && !Array.isArray(payload.metaData)) {
481
+ return {
482
+ ...payload,
483
+ metaData: {
484
+ ...payload.metaData,
485
+ payloadTruncated: true
486
+ }
487
+ };
488
+ }
489
+ return {
490
+ ...payload,
491
+ payloadTruncated: true
492
+ };
493
+ }
494
+ function truncatePayload(payload, truncateStringLength) {
495
+ const cloned = clonePayload(payload);
496
+ if (!cloned) return void 0;
497
+ return markPayloadTruncated(truncateValue(cloned, truncateStringLength));
498
+ }
499
+ function getPayloadMessageId(payload) {
500
+ const value = payload.metaData?.requestId ?? payload.messageId;
501
+ return value === void 0 ? void 0 : String(value);
502
+ }
503
+ function preparePayloadForSizePolicy(payload, options = {}) {
504
+ const maxPayloadBytes = options.maxPayloadBytes ?? 64e3;
505
+ const payloadSizePolicy = options.payloadSizePolicy ?? "drop";
506
+ const payloadTruncateStringLength = options.payloadTruncateStringLength ?? 1024;
507
+ if (payloadByteLength(payload) <= maxPayloadBytes) {
508
+ return { ok: true, payload, truncated: false };
509
+ }
510
+ if (payloadSizePolicy === "truncate") {
511
+ const truncatedPayload = truncatePayload(payload, payloadTruncateStringLength);
512
+ if (truncatedPayload && payloadByteLength(truncatedPayload) <= maxPayloadBytes) {
513
+ return { ok: true, payload: truncatedPayload, truncated: true };
514
+ }
515
+ }
516
+ const messageId = getPayloadMessageId(payload);
517
+ return messageId === void 0 ? { ok: false, reason: "payload_too_large" } : { ok: false, reason: "payload_too_large", messageId };
518
+ }
519
+
104
520
  // src/browser/core/delivery.ts
105
521
  function createId(prefix) {
106
522
  return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
@@ -132,7 +548,6 @@ function createDeliveryManager(config) {
132
548
  const lockTtlMs = config.lockTtlMs ?? 5e3;
133
549
  const tabId = config.tabId ?? createId("tab");
134
550
  const batchSize = config.batchSize ?? 25;
135
- const maxPayloadBytes = config.maxPayloadBytes ?? 64e3;
136
551
  function recordDrop(event) {
137
552
  diagnostics.dropped.push(event);
138
553
  config.onDrop?.(event);
@@ -141,12 +556,22 @@ function createDeliveryManager(config) {
141
556
  diagnostics.lastError = error.message;
142
557
  config.onError?.(error);
143
558
  }
144
- function payloadByteLength(payload) {
145
- const serialized = JSON.stringify(payload);
146
- if (typeof TextEncoder !== "undefined") {
147
- return new TextEncoder().encode(serialized).length;
559
+ function preparePayloadForSend(payload) {
560
+ const sizePolicyOptions = {};
561
+ if (config.maxPayloadBytes !== void 0) sizePolicyOptions.maxPayloadBytes = config.maxPayloadBytes;
562
+ if (config.payloadSizePolicy !== void 0) sizePolicyOptions.payloadSizePolicy = config.payloadSizePolicy;
563
+ if (config.payloadTruncateStringLength !== void 0) {
564
+ sizePolicyOptions.payloadTruncateStringLength = config.payloadTruncateStringLength;
148
565
  }
149
- return serialized.length;
566
+ const prepared = preparePayloadForSizePolicy(payload, sizePolicyOptions);
567
+ if (prepared.ok) return prepared.payload;
568
+ const diagnostic = {
569
+ reason: prepared.reason,
570
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
571
+ };
572
+ if (prepared.messageId !== void 0) diagnostic.messageId = prepared.messageId;
573
+ recordDrop(diagnostic);
574
+ return void 0;
150
575
  }
151
576
  function recordStorageUnavailable() {
152
577
  recordDrop({
@@ -236,7 +661,19 @@ function createDeliveryManager(config) {
236
661
  idempotencyKey: String(payload.idempotencyKey ?? createIdempotencyKey(payload, messageId))
237
662
  };
238
663
  }
664
+ function tryBeacon(body) {
665
+ if (!config.useBeacon) return false;
666
+ const sendBeacon = globalThis.navigator?.sendBeacon;
667
+ if (typeof sendBeacon !== "function") return false;
668
+ try {
669
+ const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
670
+ return sendBeacon.call(globalThis.navigator, config.endpoint, blob);
671
+ } catch {
672
+ return false;
673
+ }
674
+ }
239
675
  async function deliver(body) {
676
+ if (tryBeacon(body)) return;
240
677
  const fetchImpl = config.fetch ?? globalThis.fetch;
241
678
  if (typeof fetchImpl !== "function") {
242
679
  throw new Error("fetch_unavailable");
@@ -253,26 +690,22 @@ function createDeliveryManager(config) {
253
690
  return {
254
691
  async send(payload) {
255
692
  const body = withEnvelope(payload);
256
- if (payloadByteLength(body) > maxPayloadBytes) {
257
- recordDrop({
258
- messageId: String(body.metaData?.requestId ?? body.messageId),
259
- reason: "payload_too_large",
260
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
261
- });
693
+ const preparedBody = preparePayloadForSend(body);
694
+ if (!preparedBody) {
262
695
  return;
263
696
  }
264
697
  diagnostics.inFlight += 1;
265
698
  try {
266
- await deliver(body);
699
+ await deliver(preparedBody);
267
700
  } catch (error) {
268
701
  const deliveryError = error instanceof Error ? error : new Error(String(error));
269
702
  recordError(deliveryError);
270
703
  enqueue({
271
- messageId: String(body.metaData?.requestId ?? body.messageId),
704
+ messageId: String(getPayloadMessageId(body)),
272
705
  savedAt: Date.now(),
273
706
  attempts: 1,
274
707
  lastError: deliveryError.message,
275
- payload: body
708
+ payload: preparedBody
276
709
  });
277
710
  } finally {
278
711
  diagnostics.inFlight -= 1;
@@ -385,60 +818,268 @@ function createDeliveryManager(config) {
385
818
  };
386
819
  }
387
820
 
388
- // src/browser/core/privacy.ts
389
- var sensitiveKeys = /* @__PURE__ */ new Set([
390
- "email",
391
- "phone",
392
- "mobile",
393
- "address",
394
- "address1",
395
- "address2",
396
- "first_name",
397
- "last_name",
398
- "name",
399
- "token",
400
- "secret",
401
- "password",
402
- "session",
403
- "cookie"
404
- ]);
405
- function sanitizeValue(value, allow) {
406
- if (Array.isArray(value)) {
407
- return value.map((item) => sanitizeValue(item, allow));
821
+ // src/browser/core/identity.ts
822
+ var ANONYMOUS_ID_STORAGE_KEY = "dmd_anonymous_id";
823
+ var LEGACY_ANONYMOUS_ID_STORAGE_KEY = "anonymousId";
824
+ function getUrlParam(name) {
825
+ const href = getBrowserWindow()?.location?.href;
826
+ if (!href) return null;
827
+ try {
828
+ return new URL(href).searchParams.get(name);
829
+ } catch {
830
+ return null;
408
831
  }
409
- if (value && typeof value === "object") {
410
- return Object.fromEntries(
411
- Object.entries(value).filter(([key]) => !sensitiveKeys.has(key.toLowerCase()) || allow.has(key.toLowerCase())).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow)])
412
- );
832
+ }
833
+ function resolveAnonymousId(persistence) {
834
+ const urlAnonymousId = getUrlParam("aid");
835
+ if (isUuid(urlAnonymousId)) {
836
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, urlAnonymousId);
837
+ return urlAnonymousId;
413
838
  }
414
- return value;
839
+ const storedAnonymousId = persistence.getItem(ANONYMOUS_ID_STORAGE_KEY);
840
+ if (isUuid(storedAnonymousId)) {
841
+ return storedAnonymousId;
842
+ }
843
+ const legacyAnonymousId = persistence.getItem(LEGACY_ANONYMOUS_ID_STORAGE_KEY);
844
+ if (isUuid(legacyAnonymousId)) {
845
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, legacyAnonymousId);
846
+ return legacyAnonymousId;
847
+ }
848
+ const anonymousId = createUuid();
849
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
850
+ return anonymousId;
415
851
  }
416
- function sanitizeProperties(input, allowRawKeys = []) {
417
- const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
418
- return sanitizeValue(input, allow);
852
+ function resetAnonymousId(persistence) {
853
+ const anonymousId = createUuid();
854
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
855
+ return anonymousId;
419
856
  }
420
857
 
421
- // src/browser/core/schema.ts
422
- var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
423
- function validateEventEnvelope(envelope, options = {}) {
424
- if (options.mode === "off") return { ok: true, errors: [] };
425
- const errors = [];
426
- if (typeof envelope.type !== "string") errors.push("type must be a string");
427
- if (envelope.type === "track" && typeof envelope.event !== "string") {
428
- errors.push("track event must include event name");
858
+ // src/browser/core/persistence.ts
859
+ function createPersistenceHealth(requested) {
860
+ return {
861
+ requested,
862
+ cookieFallbackUsed: false,
863
+ memoryFallbackUsed: requested === "memory",
864
+ failures: []
865
+ };
866
+ }
867
+ function recordFailure(health, backend, operation, error) {
868
+ health.failures.push({
869
+ backend,
870
+ operation,
871
+ message: error instanceof Error ? error.message : String(error)
872
+ });
873
+ if (health.failures.length > 25) {
874
+ health.failures.shift();
429
875
  }
430
- if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
431
- if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
432
- for (const key of Object.keys(envelope.properties ?? {})) {
433
- if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
876
+ }
877
+ function createMemoryPersistence(health = createPersistenceHealth("memory")) {
878
+ const memory = {};
879
+ return {
880
+ getItem(key) {
881
+ return Object.prototype.hasOwnProperty.call(memory, key) ? memory[key] ?? null : null;
882
+ },
883
+ setItem(key, value) {
884
+ memory[key] = value;
885
+ return true;
886
+ },
887
+ removeItem(key) {
888
+ delete memory[key];
889
+ },
890
+ getHealth() {
891
+ return {
892
+ ...health,
893
+ failures: health.failures.map((failure) => ({ ...failure }))
894
+ };
895
+ }
896
+ };
897
+ }
898
+ function getCookie2(name) {
899
+ const browserWindow = getBrowserWindow();
900
+ const cookie = browserWindow?.document?.cookie;
901
+ if (!cookie) return null;
902
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
903
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
904
+ return match ? decodeURIComponent(match[1] ?? "") : null;
905
+ }
906
+ function setCookie(name, value) {
907
+ const browserWindow = getBrowserWindow();
908
+ if (!browserWindow?.document) return false;
909
+ try {
910
+ const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toUTCString();
911
+ const secure = browserWindow.location?.protocol === "https:" ? "; Secure" : "";
912
+ browserWindow.document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/${secure}; SameSite=Lax`;
913
+ return true;
914
+ } catch {
915
+ return false;
916
+ }
917
+ }
918
+ function removeCookie(name) {
919
+ const browserWindow = getBrowserWindow();
920
+ if (!browserWindow?.document) return;
921
+ browserWindow.document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
922
+ }
923
+ function getLocalStorage() {
924
+ const browserWindow = getBrowserWindow();
925
+ try {
926
+ return browserWindow?.localStorage;
927
+ } catch {
928
+ return void 0;
929
+ }
930
+ }
931
+ function getSessionStorage() {
932
+ const browserWindow = getBrowserWindow();
933
+ try {
934
+ return browserWindow?.sessionStorage;
935
+ } catch {
936
+ return void 0;
434
937
  }
435
- return { ok: errors.length === 0, errors };
938
+ }
939
+ function createBrowserPersistence(mode = "localStorage+cookie") {
940
+ const health = createPersistenceHealth(mode);
941
+ const memory = createMemoryPersistence(health);
942
+ if (mode === "none") {
943
+ return {
944
+ getItem() {
945
+ return null;
946
+ },
947
+ setItem() {
948
+ return false;
949
+ },
950
+ removeItem() {
951
+ },
952
+ getHealth() {
953
+ return {
954
+ ...health,
955
+ failures: health.failures.map((failure) => ({ ...failure }))
956
+ };
957
+ }
958
+ };
959
+ }
960
+ if (mode === "memory") return memory;
961
+ const primary = mode === "sessionStorage" ? getSessionStorage() : getLocalStorage();
962
+ const useCookie = mode === "cookie" || mode === "localStorage+cookie" || !primary;
963
+ return {
964
+ getItem(key) {
965
+ try {
966
+ const stored = primary?.getItem(key);
967
+ if (stored) return stored;
968
+ } catch (error) {
969
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "get", error);
970
+ }
971
+ if (useCookie) {
972
+ const cookie = getCookie2(key);
973
+ if (cookie) {
974
+ health.cookieFallbackUsed = true;
975
+ return cookie;
976
+ }
977
+ }
978
+ const value = memory.getItem(key);
979
+ if (value !== null) health.memoryFallbackUsed = true;
980
+ return value;
981
+ },
982
+ setItem(key, value) {
983
+ let wrote = false;
984
+ try {
985
+ primary?.setItem(key, value);
986
+ wrote = primary !== void 0;
987
+ } catch (error) {
988
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "set", error);
989
+ wrote = false;
990
+ }
991
+ if (useCookie) {
992
+ const cookieWrote = setCookie(key, value);
993
+ if (cookieWrote) health.cookieFallbackUsed = true;
994
+ wrote = cookieWrote || wrote;
995
+ }
996
+ if (!wrote) {
997
+ health.memoryFallbackUsed = true;
998
+ return memory.setItem(key, value);
999
+ }
1000
+ if (mode === "localStorage+cookie") {
1001
+ memory.setItem(key, value);
1002
+ }
1003
+ return true;
1004
+ },
1005
+ removeItem(key) {
1006
+ try {
1007
+ primary?.removeItem(key);
1008
+ } catch (error) {
1009
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "remove", error);
1010
+ }
1011
+ if (useCookie) removeCookie(key);
1012
+ memory.removeItem(key);
1013
+ },
1014
+ getHealth() {
1015
+ return {
1016
+ ...health,
1017
+ failures: health.failures.map((failure) => ({ ...failure }))
1018
+ };
1019
+ }
1020
+ };
436
1021
  }
437
1022
 
438
- // src/browser/core/DriveMetaDataSDK.ts
439
- function createId2(prefix) {
440
- return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
1023
+ // src/browser/core/session.ts
1024
+ var SESSION_STORAGE_KEY = "sessionData";
1025
+ function getUrlParam2(name) {
1026
+ const href = getBrowserWindow()?.location?.href;
1027
+ if (!href) return null;
1028
+ try {
1029
+ return new URL(href).searchParams.get(name);
1030
+ } catch {
1031
+ return null;
1032
+ }
441
1033
  }
1034
+ function readStoredSession(persistence) {
1035
+ const rawSession = persistence.getItem(SESSION_STORAGE_KEY);
1036
+ if (!rawSession) return void 0;
1037
+ try {
1038
+ const parsed = JSON.parse(rawSession);
1039
+ if (isUuid(parsed.sessionId) && typeof parsed.timestamp === "number") {
1040
+ return {
1041
+ sessionId: parsed.sessionId,
1042
+ timestamp: parsed.timestamp
1043
+ };
1044
+ }
1045
+ } catch {
1046
+ persistence.removeItem(SESSION_STORAGE_KEY);
1047
+ }
1048
+ return void 0;
1049
+ }
1050
+ function writeStoredSession(persistence, sessionId, timestamp) {
1051
+ persistence.setItem(SESSION_STORAGE_KEY, JSON.stringify({ sessionId, timestamp }));
1052
+ }
1053
+ function createSessionManager(persistence, idleTimeoutSeconds = 30 * 60) {
1054
+ function resolveSessionId() {
1055
+ const now = Date.now();
1056
+ const urlSessionId = getUrlParam2("sid") ?? getUrlParam2("session_id");
1057
+ if (isUuid(urlSessionId)) {
1058
+ writeStoredSession(persistence, urlSessionId, now);
1059
+ return urlSessionId;
1060
+ }
1061
+ const storedSession = readStoredSession(persistence);
1062
+ if (storedSession && now - storedSession.timestamp <= idleTimeoutSeconds * 1e3) {
1063
+ writeStoredSession(persistence, storedSession.sessionId, now);
1064
+ return storedSession.sessionId;
1065
+ }
1066
+ const sessionId = createUuid();
1067
+ writeStoredSession(persistence, sessionId, now);
1068
+ return sessionId;
1069
+ }
1070
+ return {
1071
+ getSessionId() {
1072
+ return resolveSessionId();
1073
+ },
1074
+ reset() {
1075
+ const sessionId = createUuid();
1076
+ writeStoredSession(persistence, sessionId, Date.now());
1077
+ return sessionId;
1078
+ }
1079
+ };
1080
+ }
1081
+
1082
+ // src/browser/core/DriveMetaDataSDK.ts
442
1083
  function endpointFromConfig(config) {
443
1084
  const host = config.apiHost ?? "https://sdk.drivemetadata.com/v2";
444
1085
  return `${host.replace(/\/$/, "")}/data-collector`;
@@ -449,14 +1090,6 @@ function requireConfigString(value, field) {
449
1090
  }
450
1091
  return value;
451
1092
  }
452
- function getBrowserStorage() {
453
- const browserWindow = getBrowserWindow();
454
- try {
455
- return browserWindow?.localStorage;
456
- } catch {
457
- return void 0;
458
- }
459
- }
460
1093
  var DriveMetaDataSDK = class {
461
1094
  constructor(config) {
462
1095
  this.initialized = true;
@@ -469,30 +1102,35 @@ var DriveMetaDataSDK = class {
469
1102
  requireConfigString(config.writeKey || config.token, "writeKey or token");
470
1103
  this.config = config;
471
1104
  this.endpoint = endpointFromConfig(config);
472
- const storage = getBrowserStorage();
1105
+ this.persistence = createBrowserPersistence(config.persistence);
1106
+ this.session = createSessionManager(this.persistence, config.sessionIdleTimeoutSeconds);
473
1107
  const deliveryConfig = {
474
- endpoint: this.endpoint
1108
+ endpoint: this.endpoint,
1109
+ storage: this.persistence
475
1110
  };
476
1111
  if (config.delivery?.maxQueueSize !== void 0) deliveryConfig.maxQueueSize = config.delivery.maxQueueSize;
477
1112
  if (config.delivery?.queueTtlMs !== void 0) deliveryConfig.queueTtlMs = config.delivery.queueTtlMs;
478
1113
  if (config.delivery?.retryDelayMs !== void 0) deliveryConfig.retryDelayMs = config.delivery.retryDelayMs;
479
1114
  if (config.delivery?.maxRetryDelayMs !== void 0) deliveryConfig.maxRetryDelayMs = config.delivery.maxRetryDelayMs;
480
1115
  if (config.delivery?.maxPayloadBytes !== void 0) deliveryConfig.maxPayloadBytes = config.delivery.maxPayloadBytes;
1116
+ if (config.delivery?.payloadSizePolicy !== void 0) deliveryConfig.payloadSizePolicy = config.delivery.payloadSizePolicy;
1117
+ if (config.delivery?.payloadTruncateStringLength !== void 0) {
1118
+ deliveryConfig.payloadTruncateStringLength = config.delivery.payloadTruncateStringLength;
1119
+ }
1120
+ if (config.delivery?.useBeacon !== void 0) deliveryConfig.useBeacon = config.delivery.useBeacon;
481
1121
  if (config.delivery?.batchSize !== void 0) deliveryConfig.batchSize = config.delivery.batchSize;
482
1122
  if (config.onDrop !== void 0) deliveryConfig.onDrop = config.onDrop;
483
1123
  if (config.onError !== void 0) deliveryConfig.onError = config.onError;
484
- this.delivery = createDeliveryManager(storage ? {
485
- ...deliveryConfig,
486
- storage
487
- } : deliveryConfig);
1124
+ this.delivery = createDeliveryManager(deliveryConfig);
488
1125
  this.initialRetryDelayMs = config.delivery?.retryDelayMs ?? 1e3;
489
1126
  this.retryDelayMs = this.initialRetryDelayMs;
490
1127
  this.maxRetryDelayMs = config.delivery?.maxRetryDelayMs ?? 3e4;
491
1128
  this.writeKey = config.writeKey || config.token || "";
492
- this.identity = { anonymousId: createId2("anon") };
493
- this.sessionId = createId2("session");
1129
+ this.identity = { anonymousId: resolveAnonymousId(this.persistence) };
1130
+ captureAttributionFromUrl(this.persistence);
494
1131
  this.consentState = normalizeConsent(config.gdprConsent ?? config.consent);
495
1132
  this.gdprConsent = this.consentState.analytics;
1133
+ this.installAutocapture();
496
1134
  if (!config.delivery?.disableLifecycleFlush) {
497
1135
  this.installLifecycleFlush();
498
1136
  }
@@ -532,7 +1170,8 @@ var DriveMetaDataSDK = class {
532
1170
  }
533
1171
  }
534
1172
  reset() {
535
- this.identity = { anonymousId: createId2("anon") };
1173
+ this.identity = { anonymousId: resetAnonymousId(this.persistence) };
1174
+ this.session.reset();
536
1175
  this.queue = [];
537
1176
  this.offline = false;
538
1177
  if (this.retryTimer !== void 0) {
@@ -541,8 +1180,20 @@ var DriveMetaDataSDK = class {
541
1180
  }
542
1181
  this.lifecycleCleanup?.();
543
1182
  this.lifecycleCleanup = void 0;
1183
+ this.autocaptureCleanup?.();
1184
+ this.autocaptureCleanup = void 0;
544
1185
  this.delivery.clearQueue("manual_clear");
545
1186
  }
1187
+ disposeForTests() {
1188
+ if (this.retryTimer !== void 0) {
1189
+ clearTimeout(this.retryTimer);
1190
+ this.retryTimer = void 0;
1191
+ }
1192
+ this.lifecycleCleanup?.();
1193
+ this.lifecycleCleanup = void 0;
1194
+ this.autocaptureCleanup?.();
1195
+ this.autocaptureCleanup = void 0;
1196
+ }
546
1197
  setConsent(consent) {
547
1198
  this.consentState = typeof consent === "object" ? mergeConsent(this.consentState, consent) : normalizeConsent(consent);
548
1199
  this.gdprConsent = this.consentState.analytics;
@@ -557,6 +1208,7 @@ var DriveMetaDataSDK = class {
557
1208
  initialized: this.initialized,
558
1209
  consent: this.gdprConsent,
559
1210
  consentPurposes: this.consentState,
1211
+ persistence: this.persistence.getHealth(),
560
1212
  queueSize: deliveryDiagnostics.queued,
561
1213
  offline: this.offline || deliveryDiagnostics.queued > 0,
562
1214
  droppedEvents: this.droppedEvents + deliveryDiagnostics.dropped.length,
@@ -571,7 +1223,9 @@ var DriveMetaDataSDK = class {
571
1223
  void this.delivery.send(payload).then(() => {
572
1224
  const diagnostics = this.delivery.getDiagnostics();
573
1225
  this.offline = diagnostics.queued > 0;
574
- this.lastError = diagnostics.lastError;
1226
+ if (diagnostics.lastError !== void 0) {
1227
+ this.lastError = diagnostics.lastError;
1228
+ }
575
1229
  if (diagnostics.queued > 0) {
576
1230
  this.scheduleRetryFlush();
577
1231
  }
@@ -603,6 +1257,28 @@ var DriveMetaDataSDK = class {
603
1257
  }
604
1258
  };
605
1259
  }
1260
+ installAutocapture() {
1261
+ const browserWindow = getBrowserWindow();
1262
+ if (!browserWindow) return;
1263
+ if (this.config.autocapture === false) return;
1264
+ const capturePageview = this.config.capturePageview !== false;
1265
+ const capturePageleave = this.config.capturePageleave !== false;
1266
+ const controller = installAutocapture({
1267
+ browserWindow,
1268
+ capturePageview,
1269
+ capturePageleave,
1270
+ onPageView: () => {
1271
+ this.page();
1272
+ },
1273
+ onPageLeave: () => {
1274
+ this.trackEvent("page_leave", { url: browserWindow.location.href });
1275
+ },
1276
+ onRouteChange: () => {
1277
+ captureAttributionFromUrl(this.persistence);
1278
+ }
1279
+ });
1280
+ this.autocaptureCleanup = controller.cleanup;
1281
+ }
606
1282
  scheduleRetryFlush() {
607
1283
  if (this.retryTimer !== void 0) return;
608
1284
  const delay = Math.min(this.retryDelayMs, this.maxRetryDelayMs);
@@ -628,7 +1304,7 @@ var DriveMetaDataSDK = class {
628
1304
  type,
629
1305
  event,
630
1306
  properties: sanitizeProperties(properties),
631
- messageId: options.messageId ?? createId2("msg"),
1307
+ messageId: ensureUuid(options.messageId),
632
1308
  timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
633
1309
  context: options.context ?? {},
634
1310
  anonymousId: this.identity.anonymousId,
@@ -637,7 +1313,7 @@ var DriveMetaDataSDK = class {
637
1313
  appId: this.config.appId,
638
1314
  writeKey: this.writeKey,
639
1315
  consent: this.consentState,
640
- sessionId: this.sessionId
1316
+ sessionId: this.session.getSessionId()
641
1317
  };
642
1318
  if (this.identity.userId !== void 0) prepared.userId = this.identity.userId;
643
1319
  if (this.identity.groupId !== void 0) prepared.groupId = this.identity.groupId;
@@ -661,7 +1337,17 @@ var DriveMetaDataSDK = class {
661
1337
  if (!validation.ok) {
662
1338
  this.lastError = `DMD SDK schema warning: ${validation.errors.join(", ")}`;
663
1339
  }
664
- this.sendEvent(this.toCollectorPayload(prepared));
1340
+ const collectorPayload = this.toCollectorPayload(prepared);
1341
+ const backendValidation = validateBackendCollectorPayload(collectorPayload);
1342
+ if (!backendValidation.ok && this.config.schemaValidation === "strict") {
1343
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1344
+ this.recordDrop(type, "invalid_payload", event);
1345
+ return;
1346
+ }
1347
+ if (!backendValidation.ok) {
1348
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1349
+ }
1350
+ this.sendEvent(collectorPayload);
665
1351
  }
666
1352
  toCollectorPayload(prepared) {
667
1353
  const browserWindow = getBrowserWindow();
@@ -677,14 +1363,14 @@ var DriveMetaDataSDK = class {
677
1363
  ua: browserWindow?.navigator?.userAgent,
678
1364
  appId: prepared.appId,
679
1365
  pageUrl: browserWindow?.location?.href,
680
- eventData: {
1366
+ eventData: mergeStoredAttribution({
681
1367
  ...prepared.properties,
682
1368
  anonymousId: prepared.anonymousId,
683
1369
  sessionId: prepared.sessionId,
684
1370
  timestamp: prepared.timestamp,
685
1371
  requestSentAt: prepared.timestamp,
686
1372
  requestReceivedAt: prepared.timestamp
687
- }
1373
+ }, this.persistence)
688
1374
  });
689
1375
  }
690
1376
  recordDrop(type, reason, event) {
@@ -760,7 +1446,7 @@ function initDmdSDK(config) {
760
1446
  return publicSingleton;
761
1447
  }
762
1448
  try {
763
- const instance = new DriveMetaDataSDK(config);
1449
+ const instance = new DriveMetaDataSDK(normalizeBrowserConfig(config));
764
1450
  return setSingleton(instance);
765
1451
  } catch (error) {
766
1452
  lastError = error instanceof Error ? error.message : String(error);