@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.
@@ -47,12 +47,62 @@ module.exports = __toCommonJS(react_exports);
47
47
  // src/react/DmdProvider.tsx
48
48
  var import_react = __toESM(require("react"), 1);
49
49
 
50
+ // src/browser/core/browser-config.ts
51
+ function setIfDefined(target, key, value) {
52
+ if (value !== void 0) {
53
+ target[key] = value;
54
+ }
55
+ }
56
+ function normalizeBrowserConfig(input) {
57
+ const normalized = {
58
+ ...input,
59
+ clientId: input.clientId ?? "",
60
+ workspaceId: input.workspaceId ?? "",
61
+ appId: input.appId ?? ""
62
+ };
63
+ setIfDefined(normalized, "apiHost", input.apiHost);
64
+ setIfDefined(normalized, "capturePageview", input.capturePageview);
65
+ setIfDefined(normalized, "capturePageleave", input.capturePageleave);
66
+ setIfDefined(normalized, "captureDeadClicks", input.captureDeadClicks);
67
+ setIfDefined(normalized, "crossSubdomainCookie", input.crossSubdomainCookie);
68
+ setIfDefined(normalized, "sessionIdleTimeoutSeconds", input.sessionIdleTimeoutSeconds);
69
+ setIfDefined(normalized, "schemaValidation", input.schemaValidation);
70
+ setIfDefined(normalized, "beforeSend", input.beforeSend);
71
+ setIfDefined(normalized, "persistence", input.disablePersistence === true ? "none" : input.persistence);
72
+ return normalized;
73
+ }
74
+
75
+ // src/core/uuid.ts
76
+ function createUuid() {
77
+ const cryptoApi = globalThis.crypto;
78
+ if (typeof cryptoApi?.randomUUID === "function") {
79
+ return cryptoApi.randomUUID();
80
+ }
81
+ const bytes = new Uint8Array(16);
82
+ if (typeof cryptoApi?.getRandomValues === "function") {
83
+ cryptoApi.getRandomValues(bytes);
84
+ } else {
85
+ for (let index = 0; index < bytes.length; index += 1) {
86
+ bytes[index] = Math.floor(Math.random() * 256);
87
+ }
88
+ }
89
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
90
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
91
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
92
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
93
+ }
94
+ function isUuid(value) {
95
+ 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);
96
+ }
97
+ function ensureUuid(value) {
98
+ return isUuid(value) ? value : createUuid();
99
+ }
100
+
50
101
  // src/core/backend-payload.ts
51
102
  function formatUtcTimestamp(value) {
52
- if (typeof value !== "string") return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
53
- const date = new Date(value);
54
- if (Number.isNaN(date.getTime())) return value;
55
- return date.toISOString().replace("T", " ").slice(0, 19);
103
+ const date = typeof value === "string" || typeof value === "number" || value instanceof Date ? new Date(value) : /* @__PURE__ */ new Date();
104
+ const safeDate = Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
105
+ return safeDate.toISOString().replace("T", " ").slice(0, 19);
56
106
  }
57
107
  function cleanObject(value) {
58
108
  if (Array.isArray(value)) {
@@ -83,15 +133,19 @@ function createBackendCollectorPayload(input) {
83
133
  };
84
134
  const metaData = {
85
135
  ...normalizedEventData,
86
- requestId: input.requestId,
136
+ requestId: ensureUuid(typeof input.requestId === "string" ? input.requestId : void 0),
87
137
  timestamp,
88
138
  eventType: input.eventType,
89
139
  requestFrom: input.requestFrom ?? "3",
90
140
  clientId: input.clientId,
91
141
  workspaceId: input.workspaceId,
92
142
  token: input.token,
93
- anonymousId: eventData.anonymousId ?? input.anonymousId,
94
- sessionId: eventData.sessionId ?? input.sessionId,
143
+ anonymousId: ensureUuid(
144
+ typeof eventData.anonymousId === "string" ? eventData.anonymousId : typeof input.anonymousId === "string" ? input.anonymousId : void 0
145
+ ),
146
+ sessionId: ensureUuid(
147
+ typeof eventData.sessionId === "string" ? eventData.sessionId : typeof input.sessionId === "string" ? input.sessionId : void 0
148
+ ),
95
149
  ua: input.ua,
96
150
  appDetails: { app_id: input.appId },
97
151
  page: { ...page2, url: page2.url ?? input.pageUrl },
@@ -102,12 +156,45 @@ function createBackendCollectorPayload(input) {
102
156
  return cleanObject(payload);
103
157
  }
104
158
 
105
- // src/core/environment.ts
106
- function getBrowserWindow() {
107
- return typeof window === "undefined" ? void 0 : window;
159
+ // src/core/backend-schema.ts
160
+ var requiredMetaDataFields = [
161
+ "requestId",
162
+ "timestamp",
163
+ "eventType",
164
+ "requestFrom",
165
+ "clientId",
166
+ "workspaceId",
167
+ "anonymousId",
168
+ "sessionId"
169
+ ];
170
+ function isPresent(value) {
171
+ return value !== null && value !== void 0 && value !== "";
172
+ }
173
+ function validateBackendCollectorPayload(payload) {
174
+ const errors = [];
175
+ const metaData = payload.metaData;
176
+ if (!metaData || typeof metaData !== "object" || Array.isArray(metaData)) {
177
+ return {
178
+ ok: false,
179
+ errors: ["metaData is required"]
180
+ };
181
+ }
182
+ const metadataRecord = metaData;
183
+ for (const field of requiredMetaDataFields) {
184
+ if (!isPresent(metadataRecord[field])) {
185
+ errors.push(`metaData.${field} is required`);
186
+ }
187
+ }
188
+ if (isPresent(metadataRecord.eventType) && typeof metadataRecord.eventType !== "string") {
189
+ errors.push("metaData.eventType must be a string");
190
+ }
191
+ return {
192
+ ok: errors.length === 0,
193
+ errors
194
+ };
108
195
  }
109
196
 
110
- // src/browser/core/consent.ts
197
+ // src/core/consent.ts
111
198
  var purposes = [
112
199
  "analytics",
113
200
  "advertising",
@@ -147,6 +234,335 @@ function canCollectPurpose(consent, purpose) {
147
234
  return consent[purpose] === "granted";
148
235
  }
149
236
 
237
+ // src/core/event-schema.ts
238
+ var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
239
+ function validateEventEnvelope(envelope, options = {}) {
240
+ if (options.mode === "off") return { ok: true, errors: [] };
241
+ const errors = [];
242
+ if (typeof envelope.type !== "string") errors.push("type must be a string");
243
+ if (envelope.type === "track" && typeof envelope.event !== "string") {
244
+ errors.push("track event must include event name");
245
+ }
246
+ if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
247
+ if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
248
+ for (const key of Object.keys(envelope.properties ?? {})) {
249
+ if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
250
+ }
251
+ return { ok: errors.length === 0, errors };
252
+ }
253
+
254
+ // src/core/environment.ts
255
+ function getBrowserWindow() {
256
+ return typeof window === "undefined" ? void 0 : window;
257
+ }
258
+
259
+ // src/core/privacy.ts
260
+ var sensitiveKeys = /* @__PURE__ */ new Set([
261
+ "email",
262
+ "phone",
263
+ "mobile",
264
+ "address",
265
+ "address1",
266
+ "address2",
267
+ "first_name",
268
+ "last_name",
269
+ "name",
270
+ "token",
271
+ "secret",
272
+ "password",
273
+ "session",
274
+ "cookie"
275
+ ]);
276
+ function sanitizeValue(value, allow, path = []) {
277
+ if (Array.isArray(value)) {
278
+ return value.map((item) => sanitizeValue(item, allow, path));
279
+ }
280
+ if (value && typeof value === "object") {
281
+ return Object.fromEntries(
282
+ Object.entries(value).filter(([key]) => {
283
+ const lowerKey = key.toLowerCase();
284
+ const lowerPath = path.map((item) => item.toLowerCase());
285
+ const isEcommerceItemName = lowerKey === "name" && lowerPath.includes("ecommerce") && lowerPath.includes("items");
286
+ return isEcommerceItemName || !sensitiveKeys.has(lowerKey) || allow.has(lowerKey);
287
+ }).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow, [...path, key])])
288
+ );
289
+ }
290
+ return value;
291
+ }
292
+ function sanitizeProperties(input, allowRawKeys = []) {
293
+ const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
294
+ return sanitizeValue(input, allow);
295
+ }
296
+
297
+ // src/core/attribution.ts
298
+ var dmdUtmKeys = [
299
+ "utm_source",
300
+ "utm_medium",
301
+ "utm_campaign",
302
+ "utm_term",
303
+ "utm_content",
304
+ "utm_id"
305
+ ];
306
+ var dmdCampaignKeys = ["campaign_id", "ad_id"];
307
+ var dmdClickIdSources = [
308
+ ["gclid", 2],
309
+ ["fbclid", 3],
310
+ ["ScCid", 1],
311
+ ["li_fat_id", 4]
312
+ ];
313
+ function cleanAttributionRecord(record) {
314
+ const cleaned = Object.fromEntries(
315
+ Object.entries(record).filter(([, value]) => value !== null && value !== void 0 && value !== "")
316
+ );
317
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
318
+ }
319
+ function objectValue(value) {
320
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
321
+ }
322
+ function mergeAttributionRecords(explicitProperties, stored) {
323
+ const attributionData = objectValue(explicitProperties.attributionData);
324
+ const utmParameter = objectValue(explicitProperties.utmParameter);
325
+ return {
326
+ ...explicitProperties,
327
+ ...stored.attributionData || attributionData ? { attributionData: { ...stored.attributionData ?? {}, ...attributionData ?? {} } } : {},
328
+ ...stored.utmParameter || utmParameter ? { utmParameter: { ...stored.utmParameter ?? {}, ...utmParameter ?? {} } } : {}
329
+ };
330
+ }
331
+
332
+ // src/browser/core/attribution.ts
333
+ function getCurrentUrl() {
334
+ return getBrowserWindow()?.location?.href;
335
+ }
336
+ function getCookie(name) {
337
+ const cookie = getBrowserWindow()?.document?.cookie;
338
+ if (!cookie) return void 0;
339
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
340
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
341
+ return match ? decodeURIComponent(match[1] ?? "") : void 0;
342
+ }
343
+ function getParams(url) {
344
+ return new URL(url, "https://placeholder.local").searchParams;
345
+ }
346
+ function setIfPresent(persistence, key, value) {
347
+ if (value !== null && value !== "") {
348
+ persistence.setItem(key, value);
349
+ }
350
+ }
351
+ function getStoredString(persistence, key) {
352
+ const value = persistence.getItem(key);
353
+ return value === null || value === "" ? void 0 : value;
354
+ }
355
+ function getStoredNumberOrString(persistence, key) {
356
+ const value = getStoredString(persistence, key);
357
+ if (value === void 0) return void 0;
358
+ const parsed = Number.parseInt(value, 10);
359
+ return Number.isNaN(parsed) ? value : parsed;
360
+ }
361
+ function captureAttributionFromUrl(persistence, url = getCurrentUrl()) {
362
+ if (!url) return;
363
+ const params = getParams(url);
364
+ for (const key of dmdUtmKeys) {
365
+ setIfPresent(persistence, key, params.get(key));
366
+ }
367
+ for (const key of dmdCampaignKeys) {
368
+ setIfPresent(persistence, key, params.get(key));
369
+ }
370
+ for (const [param, sdkPubId] of dmdClickIdSources) {
371
+ const value = params.get(param);
372
+ if (value) {
373
+ persistence.setItem("unique_id", value);
374
+ persistence.setItem("sdk_pub_id", String(sdkPubId));
375
+ }
376
+ }
377
+ }
378
+ function getStoredAttributionData(persistence) {
379
+ return cleanAttributionRecord({
380
+ unique_id: getStoredString(persistence, "unique_id"),
381
+ sdk_pub_id: getStoredNumberOrString(persistence, "sdk_pub_id"),
382
+ fbc: getStoredString(persistence, "fbc") ?? getCookie("_fbc"),
383
+ fbp: getStoredString(persistence, "fbp") ?? getCookie("_fbp"),
384
+ campaign_id: getStoredString(persistence, "campaign_id"),
385
+ ad_id: getStoredString(persistence, "ad_id")
386
+ });
387
+ }
388
+ function getStoredUtmParameter(persistence) {
389
+ return cleanAttributionRecord(Object.fromEntries(dmdUtmKeys.map((key) => [key, getStoredString(persistence, key)])));
390
+ }
391
+ function mergeStoredAttribution(properties, persistence) {
392
+ const stored = {};
393
+ const attributionData = getStoredAttributionData(persistence);
394
+ const utmParameter = getStoredUtmParameter(persistence);
395
+ if (attributionData !== void 0) stored.attributionData = attributionData;
396
+ if (utmParameter !== void 0) stored.utmParameter = utmParameter;
397
+ return mergeAttributionRecords(properties, stored);
398
+ }
399
+
400
+ // src/browser/core/autocapture.ts
401
+ function installAutocapture(config) {
402
+ const trackedPageUrls = /* @__PURE__ */ new Set();
403
+ const timers = /* @__PURE__ */ new Set();
404
+ const pageviewDelayMs = config.pageviewDelayMs ?? 500;
405
+ const history = config.browserWindow.history;
406
+ const originalPushState = history?.pushState;
407
+ const originalReplaceState = history?.replaceState;
408
+ function clearTimer(timer) {
409
+ timers.delete(timer);
410
+ clearTimeout(timer);
411
+ }
412
+ function canonicalUrl() {
413
+ return config.browserWindow.location.href;
414
+ }
415
+ function trackCurrentPageview() {
416
+ if (!config.capturePageview) return;
417
+ const url = canonicalUrl();
418
+ if (trackedPageUrls.has(url)) return;
419
+ trackedPageUrls.add(url);
420
+ config.onPageView();
421
+ }
422
+ function schedulePageview(delayMs) {
423
+ if (!config.capturePageview) return;
424
+ const timer = setTimeout(() => {
425
+ timers.delete(timer);
426
+ trackCurrentPageview();
427
+ }, delayMs);
428
+ timers.add(timer);
429
+ }
430
+ function handleRouteChange() {
431
+ config.onRouteChange?.();
432
+ trackCurrentPageview();
433
+ }
434
+ function wrapHistoryMethod(method) {
435
+ if (!history) return;
436
+ const original = history[method];
437
+ if (typeof original !== "function") return;
438
+ history[method] = function wrappedHistoryMethod(...args) {
439
+ const result = original.apply(this, args);
440
+ const timer = setTimeout(() => {
441
+ timers.delete(timer);
442
+ handleRouteChange();
443
+ }, 0);
444
+ timers.add(timer);
445
+ return result;
446
+ };
447
+ }
448
+ function handlePopOrHashChange() {
449
+ handleRouteChange();
450
+ }
451
+ function handlePageLeave() {
452
+ if (config.capturePageleave) {
453
+ config.onPageLeave();
454
+ }
455
+ }
456
+ schedulePageview(pageviewDelayMs);
457
+ wrapHistoryMethod("pushState");
458
+ wrapHistoryMethod("replaceState");
459
+ const canListen = typeof config.browserWindow.addEventListener === "function" && typeof config.browserWindow.removeEventListener === "function";
460
+ if (canListen) {
461
+ config.browserWindow.addEventListener("popstate", handlePopOrHashChange);
462
+ config.browserWindow.addEventListener("hashchange", handlePopOrHashChange);
463
+ config.browserWindow.addEventListener("beforeunload", handlePageLeave);
464
+ }
465
+ return {
466
+ cleanup() {
467
+ for (const timer of Array.from(timers)) {
468
+ clearTimer(timer);
469
+ }
470
+ if (history && originalPushState) history.pushState = originalPushState;
471
+ if (history && originalReplaceState) history.replaceState = originalReplaceState;
472
+ if (canListen) {
473
+ config.browserWindow.removeEventListener("popstate", handlePopOrHashChange);
474
+ config.browserWindow.removeEventListener("hashchange", handlePopOrHashChange);
475
+ config.browserWindow.removeEventListener("beforeunload", handlePageLeave);
476
+ }
477
+ }
478
+ };
479
+ }
480
+
481
+ // src/core/payload-size.ts
482
+ var preserveStringKeys = /* @__PURE__ */ new Set([
483
+ "requestId",
484
+ "eventType",
485
+ "requestFrom",
486
+ "clientId",
487
+ "workspaceId",
488
+ "anonymousId",
489
+ "sessionId",
490
+ "token"
491
+ ]);
492
+ function payloadByteLength(payload) {
493
+ const serialized = JSON.stringify(payload);
494
+ if (typeof TextEncoder !== "undefined") {
495
+ return new TextEncoder().encode(serialized).length;
496
+ }
497
+ return serialized.length;
498
+ }
499
+ function clonePayload(payload) {
500
+ try {
501
+ return JSON.parse(JSON.stringify(payload));
502
+ } catch {
503
+ return void 0;
504
+ }
505
+ }
506
+ function truncateValue(value, truncateStringLength, key) {
507
+ if (typeof value === "string") {
508
+ if (key && preserveStringKeys.has(key)) return value;
509
+ if (value.length <= truncateStringLength) return value;
510
+ return `${value.slice(0, truncateStringLength)}...[TRUNCATED]`;
511
+ }
512
+ if (Array.isArray(value)) {
513
+ return value.map((item) => truncateValue(item, truncateStringLength));
514
+ }
515
+ if (value && typeof value === "object") {
516
+ return Object.fromEntries(
517
+ Object.entries(value).map(([childKey, childValue]) => [
518
+ childKey,
519
+ truncateValue(childValue, truncateStringLength, childKey)
520
+ ])
521
+ );
522
+ }
523
+ return value;
524
+ }
525
+ function markPayloadTruncated(payload) {
526
+ if (payload.metaData && typeof payload.metaData === "object" && !Array.isArray(payload.metaData)) {
527
+ return {
528
+ ...payload,
529
+ metaData: {
530
+ ...payload.metaData,
531
+ payloadTruncated: true
532
+ }
533
+ };
534
+ }
535
+ return {
536
+ ...payload,
537
+ payloadTruncated: true
538
+ };
539
+ }
540
+ function truncatePayload(payload, truncateStringLength) {
541
+ const cloned = clonePayload(payload);
542
+ if (!cloned) return void 0;
543
+ return markPayloadTruncated(truncateValue(cloned, truncateStringLength));
544
+ }
545
+ function getPayloadMessageId(payload) {
546
+ const value = payload.metaData?.requestId ?? payload.messageId;
547
+ return value === void 0 ? void 0 : String(value);
548
+ }
549
+ function preparePayloadForSizePolicy(payload, options = {}) {
550
+ const maxPayloadBytes = options.maxPayloadBytes ?? 64e3;
551
+ const payloadSizePolicy = options.payloadSizePolicy ?? "drop";
552
+ const payloadTruncateStringLength = options.payloadTruncateStringLength ?? 1024;
553
+ if (payloadByteLength(payload) <= maxPayloadBytes) {
554
+ return { ok: true, payload, truncated: false };
555
+ }
556
+ if (payloadSizePolicy === "truncate") {
557
+ const truncatedPayload = truncatePayload(payload, payloadTruncateStringLength);
558
+ if (truncatedPayload && payloadByteLength(truncatedPayload) <= maxPayloadBytes) {
559
+ return { ok: true, payload: truncatedPayload, truncated: true };
560
+ }
561
+ }
562
+ const messageId = getPayloadMessageId(payload);
563
+ return messageId === void 0 ? { ok: false, reason: "payload_too_large" } : { ok: false, reason: "payload_too_large", messageId };
564
+ }
565
+
150
566
  // src/browser/core/delivery.ts
151
567
  function createId(prefix) {
152
568
  return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
@@ -178,7 +594,6 @@ function createDeliveryManager(config) {
178
594
  const lockTtlMs = config.lockTtlMs ?? 5e3;
179
595
  const tabId = config.tabId ?? createId("tab");
180
596
  const batchSize = config.batchSize ?? 25;
181
- const maxPayloadBytes = config.maxPayloadBytes ?? 64e3;
182
597
  function recordDrop(event) {
183
598
  diagnostics.dropped.push(event);
184
599
  config.onDrop?.(event);
@@ -187,12 +602,22 @@ function createDeliveryManager(config) {
187
602
  diagnostics.lastError = error.message;
188
603
  config.onError?.(error);
189
604
  }
190
- function payloadByteLength(payload) {
191
- const serialized = JSON.stringify(payload);
192
- if (typeof TextEncoder !== "undefined") {
193
- return new TextEncoder().encode(serialized).length;
605
+ function preparePayloadForSend(payload) {
606
+ const sizePolicyOptions = {};
607
+ if (config.maxPayloadBytes !== void 0) sizePolicyOptions.maxPayloadBytes = config.maxPayloadBytes;
608
+ if (config.payloadSizePolicy !== void 0) sizePolicyOptions.payloadSizePolicy = config.payloadSizePolicy;
609
+ if (config.payloadTruncateStringLength !== void 0) {
610
+ sizePolicyOptions.payloadTruncateStringLength = config.payloadTruncateStringLength;
194
611
  }
195
- return serialized.length;
612
+ const prepared = preparePayloadForSizePolicy(payload, sizePolicyOptions);
613
+ if (prepared.ok) return prepared.payload;
614
+ const diagnostic = {
615
+ reason: prepared.reason,
616
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
617
+ };
618
+ if (prepared.messageId !== void 0) diagnostic.messageId = prepared.messageId;
619
+ recordDrop(diagnostic);
620
+ return void 0;
196
621
  }
197
622
  function recordStorageUnavailable() {
198
623
  recordDrop({
@@ -282,7 +707,19 @@ function createDeliveryManager(config) {
282
707
  idempotencyKey: String(payload.idempotencyKey ?? createIdempotencyKey(payload, messageId))
283
708
  };
284
709
  }
710
+ function tryBeacon(body) {
711
+ if (!config.useBeacon) return false;
712
+ const sendBeacon = globalThis.navigator?.sendBeacon;
713
+ if (typeof sendBeacon !== "function") return false;
714
+ try {
715
+ const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
716
+ return sendBeacon.call(globalThis.navigator, config.endpoint, blob);
717
+ } catch {
718
+ return false;
719
+ }
720
+ }
285
721
  async function deliver(body) {
722
+ if (tryBeacon(body)) return;
286
723
  const fetchImpl = config.fetch ?? globalThis.fetch;
287
724
  if (typeof fetchImpl !== "function") {
288
725
  throw new Error("fetch_unavailable");
@@ -299,26 +736,22 @@ function createDeliveryManager(config) {
299
736
  return {
300
737
  async send(payload) {
301
738
  const body = withEnvelope(payload);
302
- if (payloadByteLength(body) > maxPayloadBytes) {
303
- recordDrop({
304
- messageId: String(body.metaData?.requestId ?? body.messageId),
305
- reason: "payload_too_large",
306
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
307
- });
739
+ const preparedBody = preparePayloadForSend(body);
740
+ if (!preparedBody) {
308
741
  return;
309
742
  }
310
743
  diagnostics.inFlight += 1;
311
744
  try {
312
- await deliver(body);
745
+ await deliver(preparedBody);
313
746
  } catch (error) {
314
747
  const deliveryError = error instanceof Error ? error : new Error(String(error));
315
748
  recordError(deliveryError);
316
749
  enqueue({
317
- messageId: String(body.metaData?.requestId ?? body.messageId),
750
+ messageId: String(getPayloadMessageId(body)),
318
751
  savedAt: Date.now(),
319
752
  attempts: 1,
320
753
  lastError: deliveryError.message,
321
- payload: body
754
+ payload: preparedBody
322
755
  });
323
756
  } finally {
324
757
  diagnostics.inFlight -= 1;
@@ -431,60 +864,268 @@ function createDeliveryManager(config) {
431
864
  };
432
865
  }
433
866
 
434
- // src/browser/core/privacy.ts
435
- var sensitiveKeys = /* @__PURE__ */ new Set([
436
- "email",
437
- "phone",
438
- "mobile",
439
- "address",
440
- "address1",
441
- "address2",
442
- "first_name",
443
- "last_name",
444
- "name",
445
- "token",
446
- "secret",
447
- "password",
448
- "session",
449
- "cookie"
450
- ]);
451
- function sanitizeValue(value, allow) {
452
- if (Array.isArray(value)) {
453
- return value.map((item) => sanitizeValue(item, allow));
867
+ // src/browser/core/identity.ts
868
+ var ANONYMOUS_ID_STORAGE_KEY = "dmd_anonymous_id";
869
+ var LEGACY_ANONYMOUS_ID_STORAGE_KEY = "anonymousId";
870
+ function getUrlParam(name) {
871
+ const href = getBrowserWindow()?.location?.href;
872
+ if (!href) return null;
873
+ try {
874
+ return new URL(href).searchParams.get(name);
875
+ } catch {
876
+ return null;
454
877
  }
455
- if (value && typeof value === "object") {
456
- return Object.fromEntries(
457
- Object.entries(value).filter(([key]) => !sensitiveKeys.has(key.toLowerCase()) || allow.has(key.toLowerCase())).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow)])
458
- );
878
+ }
879
+ function resolveAnonymousId(persistence) {
880
+ const urlAnonymousId = getUrlParam("aid");
881
+ if (isUuid(urlAnonymousId)) {
882
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, urlAnonymousId);
883
+ return urlAnonymousId;
459
884
  }
460
- return value;
885
+ const storedAnonymousId = persistence.getItem(ANONYMOUS_ID_STORAGE_KEY);
886
+ if (isUuid(storedAnonymousId)) {
887
+ return storedAnonymousId;
888
+ }
889
+ const legacyAnonymousId = persistence.getItem(LEGACY_ANONYMOUS_ID_STORAGE_KEY);
890
+ if (isUuid(legacyAnonymousId)) {
891
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, legacyAnonymousId);
892
+ return legacyAnonymousId;
893
+ }
894
+ const anonymousId = createUuid();
895
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
896
+ return anonymousId;
461
897
  }
462
- function sanitizeProperties(input, allowRawKeys = []) {
463
- const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
464
- return sanitizeValue(input, allow);
898
+ function resetAnonymousId(persistence) {
899
+ const anonymousId = createUuid();
900
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
901
+ return anonymousId;
465
902
  }
466
903
 
467
- // src/browser/core/schema.ts
468
- var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
469
- function validateEventEnvelope(envelope, options = {}) {
470
- if (options.mode === "off") return { ok: true, errors: [] };
471
- const errors = [];
472
- if (typeof envelope.type !== "string") errors.push("type must be a string");
473
- if (envelope.type === "track" && typeof envelope.event !== "string") {
474
- errors.push("track event must include event name");
904
+ // src/browser/core/persistence.ts
905
+ function createPersistenceHealth(requested) {
906
+ return {
907
+ requested,
908
+ cookieFallbackUsed: false,
909
+ memoryFallbackUsed: requested === "memory",
910
+ failures: []
911
+ };
912
+ }
913
+ function recordFailure(health, backend, operation, error) {
914
+ health.failures.push({
915
+ backend,
916
+ operation,
917
+ message: error instanceof Error ? error.message : String(error)
918
+ });
919
+ if (health.failures.length > 25) {
920
+ health.failures.shift();
475
921
  }
476
- if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
477
- if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
478
- for (const key of Object.keys(envelope.properties ?? {})) {
479
- if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
922
+ }
923
+ function createMemoryPersistence(health = createPersistenceHealth("memory")) {
924
+ const memory = {};
925
+ return {
926
+ getItem(key) {
927
+ return Object.prototype.hasOwnProperty.call(memory, key) ? memory[key] ?? null : null;
928
+ },
929
+ setItem(key, value) {
930
+ memory[key] = value;
931
+ return true;
932
+ },
933
+ removeItem(key) {
934
+ delete memory[key];
935
+ },
936
+ getHealth() {
937
+ return {
938
+ ...health,
939
+ failures: health.failures.map((failure) => ({ ...failure }))
940
+ };
941
+ }
942
+ };
943
+ }
944
+ function getCookie2(name) {
945
+ const browserWindow = getBrowserWindow();
946
+ const cookie = browserWindow?.document?.cookie;
947
+ if (!cookie) return null;
948
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
949
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
950
+ return match ? decodeURIComponent(match[1] ?? "") : null;
951
+ }
952
+ function setCookie(name, value) {
953
+ const browserWindow = getBrowserWindow();
954
+ if (!browserWindow?.document) return false;
955
+ try {
956
+ const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toUTCString();
957
+ const secure = browserWindow.location?.protocol === "https:" ? "; Secure" : "";
958
+ browserWindow.document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/${secure}; SameSite=Lax`;
959
+ return true;
960
+ } catch {
961
+ return false;
962
+ }
963
+ }
964
+ function removeCookie(name) {
965
+ const browserWindow = getBrowserWindow();
966
+ if (!browserWindow?.document) return;
967
+ browserWindow.document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
968
+ }
969
+ function getLocalStorage() {
970
+ const browserWindow = getBrowserWindow();
971
+ try {
972
+ return browserWindow?.localStorage;
973
+ } catch {
974
+ return void 0;
975
+ }
976
+ }
977
+ function getSessionStorage() {
978
+ const browserWindow = getBrowserWindow();
979
+ try {
980
+ return browserWindow?.sessionStorage;
981
+ } catch {
982
+ return void 0;
480
983
  }
481
- return { ok: errors.length === 0, errors };
984
+ }
985
+ function createBrowserPersistence(mode = "localStorage+cookie") {
986
+ const health = createPersistenceHealth(mode);
987
+ const memory = createMemoryPersistence(health);
988
+ if (mode === "none") {
989
+ return {
990
+ getItem() {
991
+ return null;
992
+ },
993
+ setItem() {
994
+ return false;
995
+ },
996
+ removeItem() {
997
+ },
998
+ getHealth() {
999
+ return {
1000
+ ...health,
1001
+ failures: health.failures.map((failure) => ({ ...failure }))
1002
+ };
1003
+ }
1004
+ };
1005
+ }
1006
+ if (mode === "memory") return memory;
1007
+ const primary = mode === "sessionStorage" ? getSessionStorage() : getLocalStorage();
1008
+ const useCookie = mode === "cookie" || mode === "localStorage+cookie" || !primary;
1009
+ return {
1010
+ getItem(key) {
1011
+ try {
1012
+ const stored = primary?.getItem(key);
1013
+ if (stored) return stored;
1014
+ } catch (error) {
1015
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "get", error);
1016
+ }
1017
+ if (useCookie) {
1018
+ const cookie = getCookie2(key);
1019
+ if (cookie) {
1020
+ health.cookieFallbackUsed = true;
1021
+ return cookie;
1022
+ }
1023
+ }
1024
+ const value = memory.getItem(key);
1025
+ if (value !== null) health.memoryFallbackUsed = true;
1026
+ return value;
1027
+ },
1028
+ setItem(key, value) {
1029
+ let wrote = false;
1030
+ try {
1031
+ primary?.setItem(key, value);
1032
+ wrote = primary !== void 0;
1033
+ } catch (error) {
1034
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "set", error);
1035
+ wrote = false;
1036
+ }
1037
+ if (useCookie) {
1038
+ const cookieWrote = setCookie(key, value);
1039
+ if (cookieWrote) health.cookieFallbackUsed = true;
1040
+ wrote = cookieWrote || wrote;
1041
+ }
1042
+ if (!wrote) {
1043
+ health.memoryFallbackUsed = true;
1044
+ return memory.setItem(key, value);
1045
+ }
1046
+ if (mode === "localStorage+cookie") {
1047
+ memory.setItem(key, value);
1048
+ }
1049
+ return true;
1050
+ },
1051
+ removeItem(key) {
1052
+ try {
1053
+ primary?.removeItem(key);
1054
+ } catch (error) {
1055
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "remove", error);
1056
+ }
1057
+ if (useCookie) removeCookie(key);
1058
+ memory.removeItem(key);
1059
+ },
1060
+ getHealth() {
1061
+ return {
1062
+ ...health,
1063
+ failures: health.failures.map((failure) => ({ ...failure }))
1064
+ };
1065
+ }
1066
+ };
482
1067
  }
483
1068
 
484
- // src/browser/core/DriveMetaDataSDK.ts
485
- function createId2(prefix) {
486
- return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
1069
+ // src/browser/core/session.ts
1070
+ var SESSION_STORAGE_KEY = "sessionData";
1071
+ function getUrlParam2(name) {
1072
+ const href = getBrowserWindow()?.location?.href;
1073
+ if (!href) return null;
1074
+ try {
1075
+ return new URL(href).searchParams.get(name);
1076
+ } catch {
1077
+ return null;
1078
+ }
487
1079
  }
1080
+ function readStoredSession(persistence) {
1081
+ const rawSession = persistence.getItem(SESSION_STORAGE_KEY);
1082
+ if (!rawSession) return void 0;
1083
+ try {
1084
+ const parsed = JSON.parse(rawSession);
1085
+ if (isUuid(parsed.sessionId) && typeof parsed.timestamp === "number") {
1086
+ return {
1087
+ sessionId: parsed.sessionId,
1088
+ timestamp: parsed.timestamp
1089
+ };
1090
+ }
1091
+ } catch {
1092
+ persistence.removeItem(SESSION_STORAGE_KEY);
1093
+ }
1094
+ return void 0;
1095
+ }
1096
+ function writeStoredSession(persistence, sessionId, timestamp) {
1097
+ persistence.setItem(SESSION_STORAGE_KEY, JSON.stringify({ sessionId, timestamp }));
1098
+ }
1099
+ function createSessionManager(persistence, idleTimeoutSeconds = 30 * 60) {
1100
+ function resolveSessionId() {
1101
+ const now = Date.now();
1102
+ const urlSessionId = getUrlParam2("sid") ?? getUrlParam2("session_id");
1103
+ if (isUuid(urlSessionId)) {
1104
+ writeStoredSession(persistence, urlSessionId, now);
1105
+ return urlSessionId;
1106
+ }
1107
+ const storedSession = readStoredSession(persistence);
1108
+ if (storedSession && now - storedSession.timestamp <= idleTimeoutSeconds * 1e3) {
1109
+ writeStoredSession(persistence, storedSession.sessionId, now);
1110
+ return storedSession.sessionId;
1111
+ }
1112
+ const sessionId = createUuid();
1113
+ writeStoredSession(persistence, sessionId, now);
1114
+ return sessionId;
1115
+ }
1116
+ return {
1117
+ getSessionId() {
1118
+ return resolveSessionId();
1119
+ },
1120
+ reset() {
1121
+ const sessionId = createUuid();
1122
+ writeStoredSession(persistence, sessionId, Date.now());
1123
+ return sessionId;
1124
+ }
1125
+ };
1126
+ }
1127
+
1128
+ // src/browser/core/DriveMetaDataSDK.ts
488
1129
  function endpointFromConfig(config) {
489
1130
  const host = config.apiHost ?? "https://sdk.drivemetadata.com/v2";
490
1131
  return `${host.replace(/\/$/, "")}/data-collector`;
@@ -495,14 +1136,6 @@ function requireConfigString(value, field) {
495
1136
  }
496
1137
  return value;
497
1138
  }
498
- function getBrowserStorage() {
499
- const browserWindow = getBrowserWindow();
500
- try {
501
- return browserWindow?.localStorage;
502
- } catch {
503
- return void 0;
504
- }
505
- }
506
1139
  var DriveMetaDataSDK = class {
507
1140
  constructor(config) {
508
1141
  this.initialized = true;
@@ -515,30 +1148,35 @@ var DriveMetaDataSDK = class {
515
1148
  requireConfigString(config.writeKey || config.token, "writeKey or token");
516
1149
  this.config = config;
517
1150
  this.endpoint = endpointFromConfig(config);
518
- const storage = getBrowserStorage();
1151
+ this.persistence = createBrowserPersistence(config.persistence);
1152
+ this.session = createSessionManager(this.persistence, config.sessionIdleTimeoutSeconds);
519
1153
  const deliveryConfig = {
520
- endpoint: this.endpoint
1154
+ endpoint: this.endpoint,
1155
+ storage: this.persistence
521
1156
  };
522
1157
  if (config.delivery?.maxQueueSize !== void 0) deliveryConfig.maxQueueSize = config.delivery.maxQueueSize;
523
1158
  if (config.delivery?.queueTtlMs !== void 0) deliveryConfig.queueTtlMs = config.delivery.queueTtlMs;
524
1159
  if (config.delivery?.retryDelayMs !== void 0) deliveryConfig.retryDelayMs = config.delivery.retryDelayMs;
525
1160
  if (config.delivery?.maxRetryDelayMs !== void 0) deliveryConfig.maxRetryDelayMs = config.delivery.maxRetryDelayMs;
526
1161
  if (config.delivery?.maxPayloadBytes !== void 0) deliveryConfig.maxPayloadBytes = config.delivery.maxPayloadBytes;
1162
+ if (config.delivery?.payloadSizePolicy !== void 0) deliveryConfig.payloadSizePolicy = config.delivery.payloadSizePolicy;
1163
+ if (config.delivery?.payloadTruncateStringLength !== void 0) {
1164
+ deliveryConfig.payloadTruncateStringLength = config.delivery.payloadTruncateStringLength;
1165
+ }
1166
+ if (config.delivery?.useBeacon !== void 0) deliveryConfig.useBeacon = config.delivery.useBeacon;
527
1167
  if (config.delivery?.batchSize !== void 0) deliveryConfig.batchSize = config.delivery.batchSize;
528
1168
  if (config.onDrop !== void 0) deliveryConfig.onDrop = config.onDrop;
529
1169
  if (config.onError !== void 0) deliveryConfig.onError = config.onError;
530
- this.delivery = createDeliveryManager(storage ? {
531
- ...deliveryConfig,
532
- storage
533
- } : deliveryConfig);
1170
+ this.delivery = createDeliveryManager(deliveryConfig);
534
1171
  this.initialRetryDelayMs = config.delivery?.retryDelayMs ?? 1e3;
535
1172
  this.retryDelayMs = this.initialRetryDelayMs;
536
1173
  this.maxRetryDelayMs = config.delivery?.maxRetryDelayMs ?? 3e4;
537
1174
  this.writeKey = config.writeKey || config.token || "";
538
- this.identity = { anonymousId: createId2("anon") };
539
- this.sessionId = createId2("session");
1175
+ this.identity = { anonymousId: resolveAnonymousId(this.persistence) };
1176
+ captureAttributionFromUrl(this.persistence);
540
1177
  this.consentState = normalizeConsent(config.gdprConsent ?? config.consent);
541
1178
  this.gdprConsent = this.consentState.analytics;
1179
+ this.installAutocapture();
542
1180
  if (!config.delivery?.disableLifecycleFlush) {
543
1181
  this.installLifecycleFlush();
544
1182
  }
@@ -578,7 +1216,8 @@ var DriveMetaDataSDK = class {
578
1216
  }
579
1217
  }
580
1218
  reset() {
581
- this.identity = { anonymousId: createId2("anon") };
1219
+ this.identity = { anonymousId: resetAnonymousId(this.persistence) };
1220
+ this.session.reset();
582
1221
  this.queue = [];
583
1222
  this.offline = false;
584
1223
  if (this.retryTimer !== void 0) {
@@ -587,8 +1226,20 @@ var DriveMetaDataSDK = class {
587
1226
  }
588
1227
  this.lifecycleCleanup?.();
589
1228
  this.lifecycleCleanup = void 0;
1229
+ this.autocaptureCleanup?.();
1230
+ this.autocaptureCleanup = void 0;
590
1231
  this.delivery.clearQueue("manual_clear");
591
1232
  }
1233
+ disposeForTests() {
1234
+ if (this.retryTimer !== void 0) {
1235
+ clearTimeout(this.retryTimer);
1236
+ this.retryTimer = void 0;
1237
+ }
1238
+ this.lifecycleCleanup?.();
1239
+ this.lifecycleCleanup = void 0;
1240
+ this.autocaptureCleanup?.();
1241
+ this.autocaptureCleanup = void 0;
1242
+ }
592
1243
  setConsent(consent) {
593
1244
  this.consentState = typeof consent === "object" ? mergeConsent(this.consentState, consent) : normalizeConsent(consent);
594
1245
  this.gdprConsent = this.consentState.analytics;
@@ -603,6 +1254,7 @@ var DriveMetaDataSDK = class {
603
1254
  initialized: this.initialized,
604
1255
  consent: this.gdprConsent,
605
1256
  consentPurposes: this.consentState,
1257
+ persistence: this.persistence.getHealth(),
606
1258
  queueSize: deliveryDiagnostics.queued,
607
1259
  offline: this.offline || deliveryDiagnostics.queued > 0,
608
1260
  droppedEvents: this.droppedEvents + deliveryDiagnostics.dropped.length,
@@ -617,7 +1269,9 @@ var DriveMetaDataSDK = class {
617
1269
  void this.delivery.send(payload).then(() => {
618
1270
  const diagnostics = this.delivery.getDiagnostics();
619
1271
  this.offline = diagnostics.queued > 0;
620
- this.lastError = diagnostics.lastError;
1272
+ if (diagnostics.lastError !== void 0) {
1273
+ this.lastError = diagnostics.lastError;
1274
+ }
621
1275
  if (diagnostics.queued > 0) {
622
1276
  this.scheduleRetryFlush();
623
1277
  }
@@ -649,6 +1303,28 @@ var DriveMetaDataSDK = class {
649
1303
  }
650
1304
  };
651
1305
  }
1306
+ installAutocapture() {
1307
+ const browserWindow = getBrowserWindow();
1308
+ if (!browserWindow) return;
1309
+ if (this.config.autocapture === false) return;
1310
+ const capturePageview = this.config.capturePageview !== false;
1311
+ const capturePageleave = this.config.capturePageleave !== false;
1312
+ const controller = installAutocapture({
1313
+ browserWindow,
1314
+ capturePageview,
1315
+ capturePageleave,
1316
+ onPageView: () => {
1317
+ this.page();
1318
+ },
1319
+ onPageLeave: () => {
1320
+ this.trackEvent("page_leave", { url: browserWindow.location.href });
1321
+ },
1322
+ onRouteChange: () => {
1323
+ captureAttributionFromUrl(this.persistence);
1324
+ }
1325
+ });
1326
+ this.autocaptureCleanup = controller.cleanup;
1327
+ }
652
1328
  scheduleRetryFlush() {
653
1329
  if (this.retryTimer !== void 0) return;
654
1330
  const delay = Math.min(this.retryDelayMs, this.maxRetryDelayMs);
@@ -674,7 +1350,7 @@ var DriveMetaDataSDK = class {
674
1350
  type,
675
1351
  event,
676
1352
  properties: sanitizeProperties(properties),
677
- messageId: options.messageId ?? createId2("msg"),
1353
+ messageId: ensureUuid(options.messageId),
678
1354
  timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
679
1355
  context: options.context ?? {},
680
1356
  anonymousId: this.identity.anonymousId,
@@ -683,7 +1359,7 @@ var DriveMetaDataSDK = class {
683
1359
  appId: this.config.appId,
684
1360
  writeKey: this.writeKey,
685
1361
  consent: this.consentState,
686
- sessionId: this.sessionId
1362
+ sessionId: this.session.getSessionId()
687
1363
  };
688
1364
  if (this.identity.userId !== void 0) prepared.userId = this.identity.userId;
689
1365
  if (this.identity.groupId !== void 0) prepared.groupId = this.identity.groupId;
@@ -707,7 +1383,17 @@ var DriveMetaDataSDK = class {
707
1383
  if (!validation.ok) {
708
1384
  this.lastError = `DMD SDK schema warning: ${validation.errors.join(", ")}`;
709
1385
  }
710
- this.sendEvent(this.toCollectorPayload(prepared));
1386
+ const collectorPayload = this.toCollectorPayload(prepared);
1387
+ const backendValidation = validateBackendCollectorPayload(collectorPayload);
1388
+ if (!backendValidation.ok && this.config.schemaValidation === "strict") {
1389
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1390
+ this.recordDrop(type, "invalid_payload", event);
1391
+ return;
1392
+ }
1393
+ if (!backendValidation.ok) {
1394
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1395
+ }
1396
+ this.sendEvent(collectorPayload);
711
1397
  }
712
1398
  toCollectorPayload(prepared) {
713
1399
  const browserWindow = getBrowserWindow();
@@ -723,14 +1409,14 @@ var DriveMetaDataSDK = class {
723
1409
  ua: browserWindow?.navigator?.userAgent,
724
1410
  appId: prepared.appId,
725
1411
  pageUrl: browserWindow?.location?.href,
726
- eventData: {
1412
+ eventData: mergeStoredAttribution({
727
1413
  ...prepared.properties,
728
1414
  anonymousId: prepared.anonymousId,
729
1415
  sessionId: prepared.sessionId,
730
1416
  timestamp: prepared.timestamp,
731
1417
  requestSentAt: prepared.timestamp,
732
1418
  requestReceivedAt: prepared.timestamp
733
- }
1419
+ }, this.persistence)
734
1420
  });
735
1421
  }
736
1422
  recordDrop(type, reason, event) {
@@ -806,7 +1492,7 @@ function initDmdSDK(config) {
806
1492
  return publicSingleton;
807
1493
  }
808
1494
  try {
809
- const instance = new DriveMetaDataSDK(config);
1495
+ const instance = new DriveMetaDataSDK(normalizeBrowserConfig(config));
810
1496
  return setSingleton(instance);
811
1497
  } catch (error) {
812
1498
  lastError = error instanceof Error ? error.message : String(error);