@shware/analytics 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/hooks/use-web-analytics.cjs +1 -1
  2. package/dist/hooks/use-web-analytics.cjs.map +1 -1
  3. package/dist/hooks/use-web-analytics.mjs +1 -1
  4. package/dist/hooks/use-web-analytics.mjs.map +1 -1
  5. package/dist/server/linkedin-conversions-api.cjs +1 -1
  6. package/dist/server/linkedin-conversions-api.cjs.map +1 -1
  7. package/dist/server/linkedin-conversions-api.mjs +1 -1
  8. package/dist/server/linkedin-conversions-api.mjs.map +1 -1
  9. package/dist/server/meta-conversions-api.cjs +6 -7
  10. package/dist/server/meta-conversions-api.cjs.map +1 -1
  11. package/dist/server/meta-conversions-api.d.cts.map +1 -1
  12. package/dist/server/meta-conversions-api.d.mts.map +1 -1
  13. package/dist/server/meta-conversions-api.mjs +6 -7
  14. package/dist/server/meta-conversions-api.mjs.map +1 -1
  15. package/dist/server/openai-conversions-api.cjs +1 -1
  16. package/dist/server/openai-conversions-api.cjs.map +1 -1
  17. package/dist/server/openai-conversions-api.mjs +1 -1
  18. package/dist/server/openai-conversions-api.mjs.map +1 -1
  19. package/dist/server/reddit-conversions-api.cjs +2 -2
  20. package/dist/server/reddit-conversions-api.cjs.map +1 -1
  21. package/dist/server/reddit-conversions-api.mjs +2 -2
  22. package/dist/server/reddit-conversions-api.mjs.map +1 -1
  23. package/dist/track/index.cjs +18 -11
  24. package/dist/track/index.cjs.map +1 -1
  25. package/dist/track/index.d.cts.map +1 -1
  26. package/dist/track/index.d.mts.map +1 -1
  27. package/dist/track/index.mjs +18 -11
  28. package/dist/track/index.mjs.map +1 -1
  29. package/dist/visitor/index.cjs +10 -5
  30. package/dist/visitor/index.cjs.map +1 -1
  31. package/dist/visitor/index.d.cts.map +1 -1
  32. package/dist/visitor/index.d.mts.map +1 -1
  33. package/dist/visitor/index.mjs +10 -5
  34. package/dist/visitor/index.mjs.map +1 -1
  35. package/dist/web/index.cjs +6 -3
  36. package/dist/web/index.cjs.map +1 -1
  37. package/dist/web/index.d.cts.map +1 -1
  38. package/dist/web/index.d.mts.map +1 -1
  39. package/dist/web/index.mjs +6 -3
  40. package/dist/web/index.mjs.map +1 -1
  41. package/package.json +1 -1
@@ -6,12 +6,12 @@ const require_utils_field = require("../utils/field.cjs");
6
6
  let _shware_utils = require("@shware/utils");
7
7
  //#region src/server/reddit-conversions-api.ts
8
8
  function getServerEvent(event, data, actionSource) {
9
- const { id, name, properties, tags, platform } = event;
9
+ const { id, name, properties, tags, platform, created_at } = event;
10
10
  const [type, params] = require_track_rdt.mapRDTEvent(name, properties, id);
11
11
  const source = require_server_action_source.resolveActionSource(platform, actionSource);
12
12
  return {
13
13
  click_id: tags.rdt_cid,
14
- event_at: Date.now(),
14
+ event_at: new Date(created_at).getTime(),
15
15
  action_source: source === "web" ? "WEBSITE" : source === "app" ? "APP" : "UNKNOWN",
16
16
  type: {
17
17
  tracking_type: type === "Custom" ? "CUSTOM" : require_track_rdt.mapServerStandardEvent(type),
@@ -1 +1 @@
1
- {"version":3,"file":"reddit-conversions-api.cjs","names":["mapRDTEvent","resolveActionSource","mapServerStandardEvent","getFirst","IGNORED_EVENTS"],"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type ServerStandardEvent, mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { type EventActionSource, resolveActionSource } from './action-source';\n\n/**\n * https://ads-api.reddit.com/docs/v3/operations/Post%20Conversion%20Events\n * https://business.reddithelp.com/s/article/map-a-catalog-to-a-signal-source\n */\nexport interface RedditEvent {\n /** Match keys: Share user identifiers to match conversions to a Reddit ad engagement. */\n click_id?: string;\n\n /** Unix epoch timestamp in milliseconds, event_at can't be older than seven days. */\n event_at: number;\n\n action_source: 'WEBSITE' | 'APP' | (string & {});\n\n type: {\n tracking_type: ServerStandardEvent | 'CUSTOM';\n custom_event_name?: string;\n };\n\n /**\n * Event metadata\n * Share as much additional information about your conversion event as you'd like. If you're\n * using the Conversions API with the pixel, conversion_id is required for deduplication.\n */\n metadata?: {\n conversion_id?: string;\n currency?: string; // ISO 4217 3-letter currency code\n item_count?: number;\n value?: number;\n products?: { id: string; name?: string; category?: string }[];\n };\n\n user?: {\n email?: string;\n external_id?: string;\n ip_address?: string;\n phone_number?: string;\n user_agent?: string;\n\n /** The Identifier for Advertisers (IDFA) of the user's Apple device. */\n idfa?: string;\n\n /** The Android Advertising ID (AAID) of the user's Android device. */\n aaid?: string;\n /**\n * The value from the first-party Pixel _rdt_uuid cookie on your domain. Note that it is in\n * the {timestamp}.{uuid} format. You may use the full value or just the UUID portion.\n * Example: 1684189007728.7c73f2ae-a433-4d7b-9838-f467da98f48e\n */\n uuid?: string;\n\n screen_dimensions?: { width: number; height: number };\n\n /**\n * A structure of data processing options to specify the processing type for the event\n * https://business.reddithelp.com/s/article/Limited-Data-Use\n */\n data_processing_options?: {\n country: string;\n region: string;\n modes: string[] | ['LDU'];\n };\n };\n}\n\nexport interface CreateRedditEventDTO {\n data: { test_id?: string; events: RedditEvent[] };\n}\n\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData,\n actionSource?: EventActionSource\n): RedditEvent {\n const { id, name, properties, tags, platform } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n // Reddit documents WEBSITE and APP only, so an offline conversion shares the fallback with an\n // undeterminable platform rather than inventing an enum value the API may reject.\n const source = resolveActionSource(platform, actionSource);\n\n return {\n click_id: tags.rdt_cid,\n event_at: Date.now(),\n action_source: source === 'web' ? 'WEBSITE' : source === 'app' ? 'APP' : 'UNKNOWN',\n type: {\n tracking_type: type === 'Custom' ? 'CUSTOM' : mapServerStandardEvent(type),\n custom_event_name: type === 'Custom' ? params.customEventName : undefined,\n },\n metadata: {\n conversion_id: id,\n currency:\n 'currency' in params && typeof params.currency === 'string'\n ? params.currency.toUpperCase()\n : undefined,\n item_count:\n 'itemCount' in params && typeof params.itemCount === 'number'\n ? params.itemCount\n : undefined,\n value: 'value' in params && typeof params.value === 'number' ? params.value : undefined,\n products:\n 'products' in params && Array.isArray(params.products) && params.products.length > 0\n ? params.products\n : undefined,\n },\n user: {\n email: getFirst(data.email),\n external_id: data.user_id,\n ip_address: data.ip_address,\n phone_number: getFirst(data.phone_number),\n user_agent: data.user_agent,\n idfa: platform === 'ios' ? tags.advertising_id : undefined,\n aaid: platform === 'android' ? tags.advertising_id : undefined,\n uuid: tags.rdt_uuid,\n screen_dimensions:\n tags.screen_width && tags.screen_height\n ? { width: tags.screen_width, height: tags.screen_height }\n : undefined,\n },\n };\n}\n\nexport async function sendEvents(\n accessToken: string,\n pixelId: string,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n testId?: string,\n actionSource?: EventActionSource\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORED_EVENTS.includes(event.name))\n .map((event) => getServerEvent(event, data, actionSource)),\n },\n };\n\n if (dto.data.events.length === 0) return;\n\n try {\n const response = await fetch(\n `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify(dto),\n }\n );\n if (response.ok) return;\n const { status } = response;\n const message = await response.text();\n console.error(`Failed to send Reddit conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send Reddit conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;;AA2EA,SAAgB,eAEd,OACA,MACA,cACa;CACb,MAAM,EAAE,IAAI,MAAM,YAAY,MAAM,aAAa;CACjD,MAAM,CAAC,MAAM,UAAUA,kBAAAA,YAAY,MAAM,YAAY,EAAE;CAGvD,MAAM,SAASC,6BAAAA,oBAAoB,UAAU,YAAY;CAEzD,OAAO;EACL,UAAU,KAAK;EACf,UAAU,KAAK,IAAI;EACnB,eAAe,WAAW,QAAQ,YAAY,WAAW,QAAQ,QAAQ;EACzE,MAAM;GACJ,eAAe,SAAS,WAAW,WAAWC,kBAAAA,uBAAuB,IAAI;GACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB,KAAA;EAClE;EACA,UAAU;GACR,eAAe;GACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B,KAAA;GACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP,KAAA;GACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ,KAAA;GAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP,KAAA;EACR;EACA,MAAM;GACJ,OAAOC,oBAAAA,SAAS,KAAK,KAAK;GAC1B,aAAa,KAAK;GAClB,YAAY,KAAK;GACjB,cAAcA,oBAAAA,SAAS,KAAK,YAAY;GACxC,YAAY,KAAK;GACjB,MAAM,aAAa,QAAQ,KAAK,iBAAiB,KAAA;GACjD,MAAM,aAAa,YAAY,KAAK,iBAAiB,KAAA;GACrD,MAAM,KAAK;GACX,mBACE,KAAK,gBAAgB,KAAK,gBACtB;IAAE,OAAO,KAAK;IAAc,QAAQ,KAAK;GAAc,IACvD,KAAA;EACR;CACF;AACF;AAEA,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA,cACA;CACA,MAAM,MAA4B,EAChC,MAAM;EACJ,SAAS;EACT,QAAQ,OACL,QAAQ,UAAU,CAACC,qCAAAA,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACvD,KAAK,UAAU,eAAe,OAAO,MAAM,YAAY,CAAC;CAC7D,EACF;CAEA,IAAI,IAAI,KAAK,OAAO,WAAW,GAAG;CAElC,IAAI;EACF,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CACf,4CAA4C,QAAQ,qBACpD;GACE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,QAAQ;IACR,eAAe,UAAU;GAC3B;GACA,MAAM,KAAK,UAAU,GAAG;EAC1B,CACF;EACA,IAAI,SAAS,IAAI;EACjB,MAAM,EAAE,WAAW;EACnB,MAAM,UAAU,MAAM,SAAS,KAAK;EACpC,QAAQ,MAAM,6CAA6C,OAAO,UAAU,SAAS;CACvF,SAAS,OAAO;EACd,QAAQ,MAAM,oDAAoD,KAAK;CACzE;AACF"}
1
+ {"version":3,"file":"reddit-conversions-api.cjs","names":["mapRDTEvent","resolveActionSource","mapServerStandardEvent","getFirst","IGNORED_EVENTS"],"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type ServerStandardEvent, mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { type EventActionSource, resolveActionSource } from './action-source';\n\n/**\n * https://ads-api.reddit.com/docs/v3/operations/Post%20Conversion%20Events\n * https://business.reddithelp.com/s/article/map-a-catalog-to-a-signal-source\n */\nexport interface RedditEvent {\n /** Match keys: Share user identifiers to match conversions to a Reddit ad engagement. */\n click_id?: string;\n\n /** Unix epoch timestamp in milliseconds, event_at can't be older than seven days. */\n event_at: number;\n\n action_source: 'WEBSITE' | 'APP' | (string & {});\n\n type: {\n tracking_type: ServerStandardEvent | 'CUSTOM';\n custom_event_name?: string;\n };\n\n /**\n * Event metadata\n * Share as much additional information about your conversion event as you'd like. If you're\n * using the Conversions API with the pixel, conversion_id is required for deduplication.\n */\n metadata?: {\n conversion_id?: string;\n currency?: string; // ISO 4217 3-letter currency code\n item_count?: number;\n value?: number;\n products?: { id: string; name?: string; category?: string }[];\n };\n\n user?: {\n email?: string;\n external_id?: string;\n ip_address?: string;\n phone_number?: string;\n user_agent?: string;\n\n /** The Identifier for Advertisers (IDFA) of the user's Apple device. */\n idfa?: string;\n\n /** The Android Advertising ID (AAID) of the user's Android device. */\n aaid?: string;\n /**\n * The value from the first-party Pixel _rdt_uuid cookie on your domain. Note that it is in\n * the {timestamp}.{uuid} format. You may use the full value or just the UUID portion.\n * Example: 1684189007728.7c73f2ae-a433-4d7b-9838-f467da98f48e\n */\n uuid?: string;\n\n screen_dimensions?: { width: number; height: number };\n\n /**\n * A structure of data processing options to specify the processing type for the event\n * https://business.reddithelp.com/s/article/Limited-Data-Use\n */\n data_processing_options?: {\n country: string;\n region: string;\n modes: string[] | ['LDU'];\n };\n };\n}\n\nexport interface CreateRedditEventDTO {\n data: { test_id?: string; events: RedditEvent[] };\n}\n\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData,\n actionSource?: EventActionSource\n): RedditEvent {\n const { id, name, properties, tags, platform, created_at } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n // Reddit documents WEBSITE and APP only, so an offline conversion shares the fallback with an\n // undeterminable platform rather than inventing an enum value the API may reject.\n const source = resolveActionSource(platform, actionSource);\n\n return {\n click_id: tags.rdt_cid,\n event_at: new Date(created_at).getTime(),\n action_source: source === 'web' ? 'WEBSITE' : source === 'app' ? 'APP' : 'UNKNOWN',\n type: {\n tracking_type: type === 'Custom' ? 'CUSTOM' : mapServerStandardEvent(type),\n custom_event_name: type === 'Custom' ? params.customEventName : undefined,\n },\n metadata: {\n conversion_id: id,\n currency:\n 'currency' in params && typeof params.currency === 'string'\n ? params.currency.toUpperCase()\n : undefined,\n item_count:\n 'itemCount' in params && typeof params.itemCount === 'number'\n ? params.itemCount\n : undefined,\n value: 'value' in params && typeof params.value === 'number' ? params.value : undefined,\n products:\n 'products' in params && Array.isArray(params.products) && params.products.length > 0\n ? params.products\n : undefined,\n },\n user: {\n email: getFirst(data.email),\n external_id: data.user_id,\n ip_address: data.ip_address,\n phone_number: getFirst(data.phone_number),\n user_agent: data.user_agent,\n idfa: platform === 'ios' ? tags.advertising_id : undefined,\n aaid: platform === 'android' ? tags.advertising_id : undefined,\n uuid: tags.rdt_uuid,\n screen_dimensions:\n tags.screen_width && tags.screen_height\n ? { width: tags.screen_width, height: tags.screen_height }\n : undefined,\n },\n };\n}\n\nexport async function sendEvents(\n accessToken: string,\n pixelId: string,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n testId?: string,\n actionSource?: EventActionSource\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORED_EVENTS.includes(event.name))\n .map((event) => getServerEvent(event, data, actionSource)),\n },\n };\n\n if (dto.data.events.length === 0) return;\n\n try {\n const response = await fetch(\n `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify(dto),\n }\n );\n if (response.ok) return;\n const { status } = response;\n const message = await response.text();\n console.error(`Failed to send Reddit conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send Reddit conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;;AA2EA,SAAgB,eAEd,OACA,MACA,cACa;CACb,MAAM,EAAE,IAAI,MAAM,YAAY,MAAM,UAAU,eAAe;CAC7D,MAAM,CAAC,MAAM,UAAUA,kBAAAA,YAAY,MAAM,YAAY,EAAE;CAGvD,MAAM,SAASC,6BAAAA,oBAAoB,UAAU,YAAY;CAEzD,OAAO;EACL,UAAU,KAAK;EACf,UAAU,IAAI,KAAK,UAAU,CAAC,CAAC,QAAQ;EACvC,eAAe,WAAW,QAAQ,YAAY,WAAW,QAAQ,QAAQ;EACzE,MAAM;GACJ,eAAe,SAAS,WAAW,WAAWC,kBAAAA,uBAAuB,IAAI;GACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB,KAAA;EAClE;EACA,UAAU;GACR,eAAe;GACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B,KAAA;GACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP,KAAA;GACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ,KAAA;GAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP,KAAA;EACR;EACA,MAAM;GACJ,OAAOC,oBAAAA,SAAS,KAAK,KAAK;GAC1B,aAAa,KAAK;GAClB,YAAY,KAAK;GACjB,cAAcA,oBAAAA,SAAS,KAAK,YAAY;GACxC,YAAY,KAAK;GACjB,MAAM,aAAa,QAAQ,KAAK,iBAAiB,KAAA;GACjD,MAAM,aAAa,YAAY,KAAK,iBAAiB,KAAA;GACrD,MAAM,KAAK;GACX,mBACE,KAAK,gBAAgB,KAAK,gBACtB;IAAE,OAAO,KAAK;IAAc,QAAQ,KAAK;GAAc,IACvD,KAAA;EACR;CACF;AACF;AAEA,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA,cACA;CACA,MAAM,MAA4B,EAChC,MAAM;EACJ,SAAS;EACT,QAAQ,OACL,QAAQ,UAAU,CAACC,qCAAAA,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACvD,KAAK,UAAU,eAAe,OAAO,MAAM,YAAY,CAAC;CAC7D,EACF;CAEA,IAAI,IAAI,KAAK,OAAO,WAAW,GAAG;CAElC,IAAI;EACF,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CACf,4CAA4C,QAAQ,qBACpD;GACE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,QAAQ;IACR,eAAe,UAAU;GAC3B;GACA,MAAM,KAAK,UAAU,GAAG;EAC1B,CACF;EACA,IAAI,SAAS,IAAI;EACjB,MAAM,EAAE,WAAW;EACnB,MAAM,UAAU,MAAM,SAAS,KAAK;EACpC,QAAQ,MAAM,6CAA6C,OAAO,UAAU,SAAS;CACvF,SAAS,OAAO;EACd,QAAQ,MAAM,oDAAoD,KAAK;CACzE;AACF"}
@@ -5,12 +5,12 @@ import { getFirst } from "../utils/field.mjs";
5
5
  import { fetch } from "@shware/utils";
6
6
  //#region src/server/reddit-conversions-api.ts
7
7
  function getServerEvent(event, data, actionSource) {
8
- const { id, name, properties, tags, platform } = event;
8
+ const { id, name, properties, tags, platform, created_at } = event;
9
9
  const [type, params] = mapRDTEvent(name, properties, id);
10
10
  const source = resolveActionSource(platform, actionSource);
11
11
  return {
12
12
  click_id: tags.rdt_cid,
13
- event_at: Date.now(),
13
+ event_at: new Date(created_at).getTime(),
14
14
  action_source: source === "web" ? "WEBSITE" : source === "app" ? "APP" : "UNKNOWN",
15
15
  type: {
16
16
  tracking_type: type === "Custom" ? "CUSTOM" : mapServerStandardEvent(type),
@@ -1 +1 @@
1
- {"version":3,"file":"reddit-conversions-api.mjs","names":[],"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type ServerStandardEvent, mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { type EventActionSource, resolveActionSource } from './action-source';\n\n/**\n * https://ads-api.reddit.com/docs/v3/operations/Post%20Conversion%20Events\n * https://business.reddithelp.com/s/article/map-a-catalog-to-a-signal-source\n */\nexport interface RedditEvent {\n /** Match keys: Share user identifiers to match conversions to a Reddit ad engagement. */\n click_id?: string;\n\n /** Unix epoch timestamp in milliseconds, event_at can't be older than seven days. */\n event_at: number;\n\n action_source: 'WEBSITE' | 'APP' | (string & {});\n\n type: {\n tracking_type: ServerStandardEvent | 'CUSTOM';\n custom_event_name?: string;\n };\n\n /**\n * Event metadata\n * Share as much additional information about your conversion event as you'd like. If you're\n * using the Conversions API with the pixel, conversion_id is required for deduplication.\n */\n metadata?: {\n conversion_id?: string;\n currency?: string; // ISO 4217 3-letter currency code\n item_count?: number;\n value?: number;\n products?: { id: string; name?: string; category?: string }[];\n };\n\n user?: {\n email?: string;\n external_id?: string;\n ip_address?: string;\n phone_number?: string;\n user_agent?: string;\n\n /** The Identifier for Advertisers (IDFA) of the user's Apple device. */\n idfa?: string;\n\n /** The Android Advertising ID (AAID) of the user's Android device. */\n aaid?: string;\n /**\n * The value from the first-party Pixel _rdt_uuid cookie on your domain. Note that it is in\n * the {timestamp}.{uuid} format. You may use the full value or just the UUID portion.\n * Example: 1684189007728.7c73f2ae-a433-4d7b-9838-f467da98f48e\n */\n uuid?: string;\n\n screen_dimensions?: { width: number; height: number };\n\n /**\n * A structure of data processing options to specify the processing type for the event\n * https://business.reddithelp.com/s/article/Limited-Data-Use\n */\n data_processing_options?: {\n country: string;\n region: string;\n modes: string[] | ['LDU'];\n };\n };\n}\n\nexport interface CreateRedditEventDTO {\n data: { test_id?: string; events: RedditEvent[] };\n}\n\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData,\n actionSource?: EventActionSource\n): RedditEvent {\n const { id, name, properties, tags, platform } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n // Reddit documents WEBSITE and APP only, so an offline conversion shares the fallback with an\n // undeterminable platform rather than inventing an enum value the API may reject.\n const source = resolveActionSource(platform, actionSource);\n\n return {\n click_id: tags.rdt_cid,\n event_at: Date.now(),\n action_source: source === 'web' ? 'WEBSITE' : source === 'app' ? 'APP' : 'UNKNOWN',\n type: {\n tracking_type: type === 'Custom' ? 'CUSTOM' : mapServerStandardEvent(type),\n custom_event_name: type === 'Custom' ? params.customEventName : undefined,\n },\n metadata: {\n conversion_id: id,\n currency:\n 'currency' in params && typeof params.currency === 'string'\n ? params.currency.toUpperCase()\n : undefined,\n item_count:\n 'itemCount' in params && typeof params.itemCount === 'number'\n ? params.itemCount\n : undefined,\n value: 'value' in params && typeof params.value === 'number' ? params.value : undefined,\n products:\n 'products' in params && Array.isArray(params.products) && params.products.length > 0\n ? params.products\n : undefined,\n },\n user: {\n email: getFirst(data.email),\n external_id: data.user_id,\n ip_address: data.ip_address,\n phone_number: getFirst(data.phone_number),\n user_agent: data.user_agent,\n idfa: platform === 'ios' ? tags.advertising_id : undefined,\n aaid: platform === 'android' ? tags.advertising_id : undefined,\n uuid: tags.rdt_uuid,\n screen_dimensions:\n tags.screen_width && tags.screen_height\n ? { width: tags.screen_width, height: tags.screen_height }\n : undefined,\n },\n };\n}\n\nexport async function sendEvents(\n accessToken: string,\n pixelId: string,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n testId?: string,\n actionSource?: EventActionSource\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORED_EVENTS.includes(event.name))\n .map((event) => getServerEvent(event, data, actionSource)),\n },\n };\n\n if (dto.data.events.length === 0) return;\n\n try {\n const response = await fetch(\n `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify(dto),\n }\n );\n if (response.ok) return;\n const { status } = response;\n const message = await response.text();\n console.error(`Failed to send Reddit conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send Reddit conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;AA2EA,SAAgB,eAEd,OACA,MACA,cACa;CACb,MAAM,EAAE,IAAI,MAAM,YAAY,MAAM,aAAa;CACjD,MAAM,CAAC,MAAM,UAAU,YAAY,MAAM,YAAY,EAAE;CAGvD,MAAM,SAAS,oBAAoB,UAAU,YAAY;CAEzD,OAAO;EACL,UAAU,KAAK;EACf,UAAU,KAAK,IAAI;EACnB,eAAe,WAAW,QAAQ,YAAY,WAAW,QAAQ,QAAQ;EACzE,MAAM;GACJ,eAAe,SAAS,WAAW,WAAW,uBAAuB,IAAI;GACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB,KAAA;EAClE;EACA,UAAU;GACR,eAAe;GACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B,KAAA;GACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP,KAAA;GACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ,KAAA;GAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP,KAAA;EACR;EACA,MAAM;GACJ,OAAO,SAAS,KAAK,KAAK;GAC1B,aAAa,KAAK;GAClB,YAAY,KAAK;GACjB,cAAc,SAAS,KAAK,YAAY;GACxC,YAAY,KAAK;GACjB,MAAM,aAAa,QAAQ,KAAK,iBAAiB,KAAA;GACjD,MAAM,aAAa,YAAY,KAAK,iBAAiB,KAAA;GACrD,MAAM,KAAK;GACX,mBACE,KAAK,gBAAgB,KAAK,gBACtB;IAAE,OAAO,KAAK;IAAc,QAAQ,KAAK;GAAc,IACvD,KAAA;EACR;CACF;AACF;AAEA,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA,cACA;CACA,MAAM,MAA4B,EAChC,MAAM;EACJ,SAAS;EACT,QAAQ,OACL,QAAQ,UAAU,CAAC,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACvD,KAAK,UAAU,eAAe,OAAO,MAAM,YAAY,CAAC;CAC7D,EACF;CAEA,IAAI,IAAI,KAAK,OAAO,WAAW,GAAG;CAElC,IAAI;EACF,MAAM,WAAW,MAAM,MACrB,4CAA4C,QAAQ,qBACpD;GACE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,QAAQ;IACR,eAAe,UAAU;GAC3B;GACA,MAAM,KAAK,UAAU,GAAG;EAC1B,CACF;EACA,IAAI,SAAS,IAAI;EACjB,MAAM,EAAE,WAAW;EACnB,MAAM,UAAU,MAAM,SAAS,KAAK;EACpC,QAAQ,MAAM,6CAA6C,OAAO,UAAU,SAAS;CACvF,SAAS,OAAO;EACd,QAAQ,MAAM,oDAAoD,KAAK;CACzE;AACF"}
1
+ {"version":3,"file":"reddit-conversions-api.mjs","names":[],"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type ServerStandardEvent, mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { type EventActionSource, resolveActionSource } from './action-source';\n\n/**\n * https://ads-api.reddit.com/docs/v3/operations/Post%20Conversion%20Events\n * https://business.reddithelp.com/s/article/map-a-catalog-to-a-signal-source\n */\nexport interface RedditEvent {\n /** Match keys: Share user identifiers to match conversions to a Reddit ad engagement. */\n click_id?: string;\n\n /** Unix epoch timestamp in milliseconds, event_at can't be older than seven days. */\n event_at: number;\n\n action_source: 'WEBSITE' | 'APP' | (string & {});\n\n type: {\n tracking_type: ServerStandardEvent | 'CUSTOM';\n custom_event_name?: string;\n };\n\n /**\n * Event metadata\n * Share as much additional information about your conversion event as you'd like. If you're\n * using the Conversions API with the pixel, conversion_id is required for deduplication.\n */\n metadata?: {\n conversion_id?: string;\n currency?: string; // ISO 4217 3-letter currency code\n item_count?: number;\n value?: number;\n products?: { id: string; name?: string; category?: string }[];\n };\n\n user?: {\n email?: string;\n external_id?: string;\n ip_address?: string;\n phone_number?: string;\n user_agent?: string;\n\n /** The Identifier for Advertisers (IDFA) of the user's Apple device. */\n idfa?: string;\n\n /** The Android Advertising ID (AAID) of the user's Android device. */\n aaid?: string;\n /**\n * The value from the first-party Pixel _rdt_uuid cookie on your domain. Note that it is in\n * the {timestamp}.{uuid} format. You may use the full value or just the UUID portion.\n * Example: 1684189007728.7c73f2ae-a433-4d7b-9838-f467da98f48e\n */\n uuid?: string;\n\n screen_dimensions?: { width: number; height: number };\n\n /**\n * A structure of data processing options to specify the processing type for the event\n * https://business.reddithelp.com/s/article/Limited-Data-Use\n */\n data_processing_options?: {\n country: string;\n region: string;\n modes: string[] | ['LDU'];\n };\n };\n}\n\nexport interface CreateRedditEventDTO {\n data: { test_id?: string; events: RedditEvent[] };\n}\n\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData,\n actionSource?: EventActionSource\n): RedditEvent {\n const { id, name, properties, tags, platform, created_at } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n // Reddit documents WEBSITE and APP only, so an offline conversion shares the fallback with an\n // undeterminable platform rather than inventing an enum value the API may reject.\n const source = resolveActionSource(platform, actionSource);\n\n return {\n click_id: tags.rdt_cid,\n event_at: new Date(created_at).getTime(),\n action_source: source === 'web' ? 'WEBSITE' : source === 'app' ? 'APP' : 'UNKNOWN',\n type: {\n tracking_type: type === 'Custom' ? 'CUSTOM' : mapServerStandardEvent(type),\n custom_event_name: type === 'Custom' ? params.customEventName : undefined,\n },\n metadata: {\n conversion_id: id,\n currency:\n 'currency' in params && typeof params.currency === 'string'\n ? params.currency.toUpperCase()\n : undefined,\n item_count:\n 'itemCount' in params && typeof params.itemCount === 'number'\n ? params.itemCount\n : undefined,\n value: 'value' in params && typeof params.value === 'number' ? params.value : undefined,\n products:\n 'products' in params && Array.isArray(params.products) && params.products.length > 0\n ? params.products\n : undefined,\n },\n user: {\n email: getFirst(data.email),\n external_id: data.user_id,\n ip_address: data.ip_address,\n phone_number: getFirst(data.phone_number),\n user_agent: data.user_agent,\n idfa: platform === 'ios' ? tags.advertising_id : undefined,\n aaid: platform === 'android' ? tags.advertising_id : undefined,\n uuid: tags.rdt_uuid,\n screen_dimensions:\n tags.screen_width && tags.screen_height\n ? { width: tags.screen_width, height: tags.screen_height }\n : undefined,\n },\n };\n}\n\nexport async function sendEvents(\n accessToken: string,\n pixelId: string,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n testId?: string,\n actionSource?: EventActionSource\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORED_EVENTS.includes(event.name))\n .map((event) => getServerEvent(event, data, actionSource)),\n },\n };\n\n if (dto.data.events.length === 0) return;\n\n try {\n const response = await fetch(\n `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify(dto),\n }\n );\n if (response.ok) return;\n const { status } = response;\n const message = await response.text();\n console.error(`Failed to send Reddit conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send Reddit conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;AA2EA,SAAgB,eAEd,OACA,MACA,cACa;CACb,MAAM,EAAE,IAAI,MAAM,YAAY,MAAM,UAAU,eAAe;CAC7D,MAAM,CAAC,MAAM,UAAU,YAAY,MAAM,YAAY,EAAE;CAGvD,MAAM,SAAS,oBAAoB,UAAU,YAAY;CAEzD,OAAO;EACL,UAAU,KAAK;EACf,UAAU,IAAI,KAAK,UAAU,CAAC,CAAC,QAAQ;EACvC,eAAe,WAAW,QAAQ,YAAY,WAAW,QAAQ,QAAQ;EACzE,MAAM;GACJ,eAAe,SAAS,WAAW,WAAW,uBAAuB,IAAI;GACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB,KAAA;EAClE;EACA,UAAU;GACR,eAAe;GACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B,KAAA;GACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP,KAAA;GACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ,KAAA;GAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP,KAAA;EACR;EACA,MAAM;GACJ,OAAO,SAAS,KAAK,KAAK;GAC1B,aAAa,KAAK;GAClB,YAAY,KAAK;GACjB,cAAc,SAAS,KAAK,YAAY;GACxC,YAAY,KAAK;GACjB,MAAM,aAAa,QAAQ,KAAK,iBAAiB,KAAA;GACjD,MAAM,aAAa,YAAY,KAAK,iBAAiB,KAAA;GACrD,MAAM,KAAK;GACX,mBACE,KAAK,gBAAgB,KAAK,gBACtB;IAAE,OAAO,KAAK;IAAc,QAAQ,KAAK;GAAc,IACvD,KAAA;EACR;CACF;AACF;AAEA,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA,cACA;CACA,MAAM,MAA4B,EAChC,MAAM;EACJ,SAAS;EACT,QAAQ,OACL,QAAQ,UAAU,CAAC,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACvD,KAAK,UAAU,eAAe,OAAO,MAAM,YAAY,CAAC;CAC7D,EACF;CAEA,IAAI,IAAI,KAAK,OAAO,WAAW,GAAG;CAElC,IAAI;EACF,MAAM,WAAW,MAAM,MACrB,4CAA4C,QAAQ,qBACpD;GACE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,QAAQ;IACR,eAAe,UAAU;GAC3B;GACA,MAAM,KAAK,UAAU,GAAG;EAC1B,CACF;EACA,IAAI,SAAS,IAAI;EACjB,MAAM,EAAE,WAAW;EACnB,MAAM,UAAU,MAAM,SAAS,KAAK;EACpC,QAAQ,MAAM,6CAA6C,OAAO,UAAU,SAAS;CACvF,SAAS,OAAO;EACd,QAAQ,MAAM,oDAAoD,KAAK;CACzE;AACF"}
@@ -79,8 +79,8 @@ async function sendEvents(events) {
79
79
  continue;
80
80
  }
81
81
  const { options, name, properties } = event;
82
- const eventId = data[index].id;
83
- options.onSucceed?.({ id: eventId });
82
+ const eventId = data.at(index)?.id;
83
+ options.onSucceed?.(eventId ? { id: eventId } : void 0);
84
84
  index++;
85
85
  if (!options.enableThirdPartyTracking || require_third_parties_ignored_events.IGNORED_EVENTS.includes(name)) continue;
86
86
  require_setup_index.config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));
@@ -94,6 +94,20 @@ const batch = 10;
94
94
  const delay = 2e3;
95
95
  const list = [];
96
96
  let timer = null;
97
+ /**
98
+ * Both paths into a send go through here, so a batch that fills up cancels the timer the
99
+ * previous push armed rather than leaving it to wake up on its own with nothing to send.
100
+ */
101
+ function flush() {
102
+ if (timer) {
103
+ clearTimeout(timer);
104
+ timer = null;
105
+ }
106
+ if (list.length === 0) return;
107
+ const copy = [...list];
108
+ list.length = 0;
109
+ sendEvents(copy);
110
+ }
97
111
  function track(name, properties, options = defaultOptions) {
98
112
  list.push({
99
113
  name,
@@ -103,18 +117,11 @@ function track(name, properties, options = defaultOptions) {
103
117
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
104
118
  });
105
119
  if (list.length >= batch) {
106
- const copy = [...list];
107
- list.length = 0;
108
- sendEvents(copy);
120
+ flush();
109
121
  return;
110
122
  }
111
123
  if (timer) clearTimeout(timer);
112
- timer = setTimeout(() => {
113
- timer = null;
114
- const copy = [...list];
115
- list.length = 0;
116
- sendEvents(copy);
117
- }, delay);
124
+ timer = setTimeout(flush, delay);
118
125
  }
119
126
  async function trackAsync(name, properties, options = defaultOptions) {
120
127
  await sendEvents([{
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","getVisitor","IGNORED_EVENTS"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = { enableThirdPartyTracking: true };\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n const session = getSession();\n if (session.isExpired()) {\n session.refresh();\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n } else {\n session.updateLastActiveTime();\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data[index].id;\n options.onSucceed?.({ id: eventId });\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n }, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n if (!cache.tags || !cache.visitor) return;\n\n const session = getSession();\n session.updateLastActiveTime();\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n tags: cache.tags,\n visitor_id: cache.visitor.id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","getVisitor","IGNORED_EVENTS"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = { enableThirdPartyTracking: true };\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n const session = getSession();\n if (session.isExpired()) {\n session.refresh();\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n } else {\n session.updateLastActiveTime();\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n if (!cache.tags || !cache.visitor) return;\n\n const session = getSession();\n session.updateLastActiveTime();\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n tags: cache.tags,\n visitor_id: cache.visitor.id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAwHG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAwBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAuIG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAwHG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAwBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAuIG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
@@ -78,8 +78,8 @@ async function sendEvents(events) {
78
78
  continue;
79
79
  }
80
80
  const { options, name, properties } = event;
81
- const eventId = data[index].id;
82
- options.onSucceed?.({ id: eventId });
81
+ const eventId = data.at(index)?.id;
82
+ options.onSucceed?.(eventId ? { id: eventId } : void 0);
83
83
  index++;
84
84
  if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) continue;
85
85
  config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));
@@ -93,6 +93,20 @@ const batch = 10;
93
93
  const delay = 2e3;
94
94
  const list = [];
95
95
  let timer = null;
96
+ /**
97
+ * Both paths into a send go through here, so a batch that fills up cancels the timer the
98
+ * previous push armed rather than leaving it to wake up on its own with nothing to send.
99
+ */
100
+ function flush() {
101
+ if (timer) {
102
+ clearTimeout(timer);
103
+ timer = null;
104
+ }
105
+ if (list.length === 0) return;
106
+ const copy = [...list];
107
+ list.length = 0;
108
+ sendEvents(copy);
109
+ }
96
110
  function track(name, properties, options = defaultOptions) {
97
111
  list.push({
98
112
  name,
@@ -102,18 +116,11 @@ function track(name, properties, options = defaultOptions) {
102
116
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
103
117
  });
104
118
  if (list.length >= batch) {
105
- const copy = [...list];
106
- list.length = 0;
107
- sendEvents(copy);
119
+ flush();
108
120
  return;
109
121
  }
110
122
  if (timer) clearTimeout(timer);
111
- timer = setTimeout(() => {
112
- timer = null;
113
- const copy = [...list];
114
- list.length = 0;
115
- sendEvents(copy);
116
- }, delay);
123
+ timer = setTimeout(flush, delay);
117
124
  }
118
125
  async function trackAsync(name, properties, options = defaultOptions) {
119
126
  await sendEvents([{
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = { enableThirdPartyTracking: true };\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n const session = getSession();\n if (session.isExpired()) {\n session.refresh();\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n } else {\n session.updateLastActiveTime();\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data[index].id;\n options.onSucceed?.({ id: eventId });\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n }, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n if (!cache.tags || !cache.visitor) return;\n\n const session = getSession();\n session.updateLastActiveTime();\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n tags: cache.tags,\n visitor_id: cache.visitor.id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = { enableThirdPartyTracking: true };\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n const session = getSession();\n if (session.isExpired()) {\n session.refresh();\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n } else {\n session.updateLastActiveTime();\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n if (!cache.tags || !cache.visitor) return;\n\n const session = getSession();\n session.updateLastActiveTime();\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n tags: cache.tags,\n visitor_id: cache.visitor.id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
@@ -12,12 +12,14 @@ async function createVisitor() {
12
12
  tags,
13
13
  properties: tags
14
14
  };
15
- const data = await (await (0, _shware_utils.fetch)(`${require_setup_index.config.endpoint}/visitors`, {
15
+ const response = await (0, _shware_utils.fetch)(`${require_setup_index.config.endpoint}/visitors`, {
16
16
  method: "POST",
17
17
  credentials: "include",
18
18
  headers: await require_setup_index.config.getHeaders(),
19
19
  body: JSON.stringify(dto)
20
- })).json();
20
+ });
21
+ if (!response.ok) throw new Error(`Failed to create visitor: ${response.status} ${await response.text()}`);
22
+ const data = await response.json();
21
23
  if (data.id) require_setup_index.config.storage.setItem(require_constants_storage.keys.visitor_id, data.id);
22
24
  return data;
23
25
  }
@@ -42,9 +44,12 @@ async function getVisitor() {
42
44
  if (require_setup_index.cache.visitor) return require_setup_index.cache.visitor;
43
45
  if (visitorFetcher) return visitorFetcher;
44
46
  visitorFetcher = getOrCreateVisitor();
45
- require_setup_index.cache.visitor = await visitorFetcher;
46
- visitorFetcher = null;
47
- return require_setup_index.cache.visitor;
47
+ try {
48
+ require_setup_index.cache.visitor = await visitorFetcher;
49
+ return require_setup_index.cache.visitor;
50
+ } finally {
51
+ visitorFetcher = null;
52
+ }
48
53
  }
49
54
  async function setVisitor(dto) {
50
55
  const { id } = await getVisitor();
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["config","keys","cache"],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n cache.visitor = await visitorFetcher;\n visitorFetcher = null;\n return cache.visitor;\n}\n\nexport async function setVisitor(dto: Omit<UpdateVisitorDTO, 'tags'>) {\n const { id } = await getVisitor();\n const tags = await config.getTags();\n const body: UpdateVisitorDTO = { ...dto, tags };\n const response = await fetch(`${config.endpoint}/visitors/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) throw new Error('Failed to set visitor');\n const data = (await response.json()) as Visitor;\n\n config.thirdPartyUserSetters.forEach((setter) => setter(body));\n cache.visitor = data;\n return data;\n}\n"],"mappings":";;;;;AAMA,eAAe,gBAAkC;CAC/C,MAAM,OAAO,MAAMA,oBAAAA,OAAO,QAAQ;CAClC,MAAM,MAAwB;EAC5B,WAAW,MAAMA,oBAAAA,OAAO,YAAY;EACpC,UAAUA,oBAAAA,OAAO;EACjB,aAAaA,oBAAAA,OAAO;EACpB;EACA,YAAY;CACd;CASA,MAAM,OAAQ,OAAM,OAAA,GAAA,cAAA,MAAA,CAPS,GAAGA,oBAAAA,OAAO,SAAS,YAAY;EAC1D,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC,EAAA,CAE4B,KAAK;CAClC,IAAI,KAAK,IACP,oBAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,YAAY,KAAK,EAAE;CAEjD,OAAO;AACT;AAEA,eAAe,qBAAuC;CACpD,MAAM,YAAYD,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,UAAU;CACxD,IAAI,aAAa,cAAc,aAAa;EAS1C,MAAM,OAAO,MAAMD,oBAAAA,OAAO,QAAQ;EAClC,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY,aAAa;GACvE,QAAQ;GACR,aAAa;GACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,EAAE,KAAK,CAA4B;EAC1D,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,cAAc;EACvC,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,IACP,oBAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,YAAY,KAAK,EAAE;EAEjD,OAAO;CACT,OACE,OAAO,cAAc;AAEzB;AAEA,IAAI,iBAA0C;AAE9C,eAAsB,aAA+B;CACnD,IAAIC,oBAAAA,MAAM,SAAS,OAAOA,oBAAAA,MAAM;CAChC,IAAI,gBAAgB,OAAO;CAC3B,iBAAiB,mBAAmB;CACpC,oBAAA,MAAM,UAAU,MAAM;CACtB,iBAAiB;CACjB,OAAOA,oBAAAA,MAAM;AACf;AAEA,eAAsB,WAAW,KAAqC;CACpE,MAAM,EAAE,OAAO,MAAM,WAAW;CAChC,MAAM,OAAO,MAAMF,oBAAAA,OAAO,QAAQ;CAClC,MAAM,OAAyB;EAAE,GAAG;EAAK;CAAK;CAC9C,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY,MAAM;EAChE,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,uBAAuB;CACzD,MAAM,OAAQ,MAAM,SAAS,KAAK;CAElC,oBAAA,OAAO,sBAAsB,SAAS,WAAW,OAAO,IAAI,CAAC;CAC7D,oBAAA,MAAM,UAAU;CAChB,OAAO;AACT"}
1
+ {"version":3,"file":"index.cjs","names":["config","keys","cache"],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create visitor: ${response.status} ${await response.text()}`);\n }\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n try {\n cache.visitor = await visitorFetcher;\n return cache.visitor;\n } finally {\n // In a `finally`, so a rejected attempt is not left in `visitorFetcher` for every later\n // caller to await again: `sendEvents` needs a visitor for every batch, and one failed\n // request would otherwise stop the page from reporting anything until it is reloaded.\n visitorFetcher = null;\n }\n}\n\nexport async function setVisitor(dto: Omit<UpdateVisitorDTO, 'tags'>) {\n const { id } = await getVisitor();\n const tags = await config.getTags();\n const body: UpdateVisitorDTO = { ...dto, tags };\n const response = await fetch(`${config.endpoint}/visitors/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) throw new Error('Failed to set visitor');\n const data = (await response.json()) as Visitor;\n\n config.thirdPartyUserSetters.forEach((setter) => setter(body));\n cache.visitor = data;\n return data;\n}\n"],"mappings":";;;;;AAMA,eAAe,gBAAkC;CAC/C,MAAM,OAAO,MAAMA,oBAAAA,OAAO,QAAQ;CAClC,MAAM,MAAwB;EAC5B,WAAW,MAAMA,oBAAAA,OAAO,YAAY;EACpC,UAAUA,oBAAAA,OAAO;EACjB,aAAaA,oBAAAA,OAAO;EACpB;EACA,YAAY;CACd;CAEA,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY;EAC1D,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC;CAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,6BAA6B,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;CAEzF,MAAM,OAAQ,MAAM,SAAS,KAAK;CAClC,IAAI,KAAK,IACP,oBAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,YAAY,KAAK,EAAE;CAEjD,OAAO;AACT;AAEA,eAAe,qBAAuC;CACpD,MAAM,YAAYD,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,UAAU;CACxD,IAAI,aAAa,cAAc,aAAa;EAS1C,MAAM,OAAO,MAAMD,oBAAAA,OAAO,QAAQ;EAClC,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY,aAAa;GACvE,QAAQ;GACR,aAAa;GACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,EAAE,KAAK,CAA4B;EAC1D,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,cAAc;EACvC,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,IACP,oBAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,YAAY,KAAK,EAAE;EAEjD,OAAO;CACT,OACE,OAAO,cAAc;AAEzB;AAEA,IAAI,iBAA0C;AAE9C,eAAsB,aAA+B;CACnD,IAAIC,oBAAAA,MAAM,SAAS,OAAOA,oBAAAA,MAAM;CAChC,IAAI,gBAAgB,OAAO;CAC3B,iBAAiB,mBAAmB;CACpC,IAAI;EACF,oBAAA,MAAM,UAAU,MAAM;EACtB,OAAOA,oBAAAA,MAAM;CACf,UAAU;EAIR,iBAAiB;CACnB;AACF;AAEA,eAAsB,WAAW,KAAqC;CACpE,MAAM,EAAE,OAAO,MAAM,WAAW;CAChC,MAAM,OAAO,MAAMF,oBAAAA,OAAO,QAAQ;CAClC,MAAM,OAAyB;EAAE,GAAG;EAAK;CAAK;CAC9C,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY,MAAM;EAChE,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,uBAAuB;CACzD,MAAM,OAAQ,MAAM,SAAS,KAAK;CAElC,oBAAA,OAAO,sBAAsB,SAAS,WAAW,OAAO,IAAI,CAAC;CAC7D,oBAAA,MAAM,UAAU;CAChB,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;iBA+DsB,cAAc,QAAQ;iBAStB,WAAW,KAAK,KAAK,4BAAyB,QAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;iBAkEsB,cAAc,QAAQ;iBAetB,WAAW,KAAK,KAAK,4BAAyB,QAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;iBA+DsB,cAAc,QAAQ;iBAStB,WAAW,KAAK,KAAK,4BAAyB,QAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;iBAkEsB,cAAc,QAAQ;iBAetB,WAAW,KAAK,KAAK,4BAAyB,QAAA"}
@@ -11,12 +11,14 @@ async function createVisitor() {
11
11
  tags,
12
12
  properties: tags
13
13
  };
14
- const data = await (await fetch(`${config.endpoint}/visitors`, {
14
+ const response = await fetch(`${config.endpoint}/visitors`, {
15
15
  method: "POST",
16
16
  credentials: "include",
17
17
  headers: await config.getHeaders(),
18
18
  body: JSON.stringify(dto)
19
- })).json();
19
+ });
20
+ if (!response.ok) throw new Error(`Failed to create visitor: ${response.status} ${await response.text()}`);
21
+ const data = await response.json();
20
22
  if (data.id) config.storage.setItem(keys.visitor_id, data.id);
21
23
  return data;
22
24
  }
@@ -41,9 +43,12 @@ async function getVisitor() {
41
43
  if (cache.visitor) return cache.visitor;
42
44
  if (visitorFetcher) return visitorFetcher;
43
45
  visitorFetcher = getOrCreateVisitor();
44
- cache.visitor = await visitorFetcher;
45
- visitorFetcher = null;
46
- return cache.visitor;
46
+ try {
47
+ cache.visitor = await visitorFetcher;
48
+ return cache.visitor;
49
+ } finally {
50
+ visitorFetcher = null;
51
+ }
47
52
  }
48
53
  async function setVisitor(dto) {
49
54
  const { id } = await getVisitor();
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n cache.visitor = await visitorFetcher;\n visitorFetcher = null;\n return cache.visitor;\n}\n\nexport async function setVisitor(dto: Omit<UpdateVisitorDTO, 'tags'>) {\n const { id } = await getVisitor();\n const tags = await config.getTags();\n const body: UpdateVisitorDTO = { ...dto, tags };\n const response = await fetch(`${config.endpoint}/visitors/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) throw new Error('Failed to set visitor');\n const data = (await response.json()) as Visitor;\n\n config.thirdPartyUserSetters.forEach((setter) => setter(body));\n cache.visitor = data;\n return data;\n}\n"],"mappings":";;;;AAMA,eAAe,gBAAkC;CAC/C,MAAM,OAAO,MAAM,OAAO,QAAQ;CAClC,MAAM,MAAwB;EAC5B,WAAW,MAAM,OAAO,YAAY;EACpC,UAAU,OAAO;EACjB,aAAa,OAAO;EACpB;EACA,YAAY;CACd;CASA,MAAM,OAAQ,OAAM,MAPG,MAAM,GAAG,OAAO,SAAS,YAAY;EAC1D,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC,EAAA,CAE4B,KAAK;CAClC,IAAI,KAAK,IACP,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,EAAE;CAEjD,OAAO;AACT;AAEA,eAAe,qBAAuC;CACpD,MAAM,YAAY,OAAO,QAAQ,QAAQ,KAAK,UAAU;CACxD,IAAI,aAAa,cAAc,aAAa;EAS1C,MAAM,OAAO,MAAM,OAAO,QAAQ;EAClC,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY,aAAa;GACvE,QAAQ;GACR,aAAa;GACb,SAAS,MAAM,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,EAAE,KAAK,CAA4B;EAC1D,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,cAAc;EACvC,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,IACP,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,EAAE;EAEjD,OAAO;CACT,OACE,OAAO,cAAc;AAEzB;AAEA,IAAI,iBAA0C;AAE9C,eAAsB,aAA+B;CACnD,IAAI,MAAM,SAAS,OAAO,MAAM;CAChC,IAAI,gBAAgB,OAAO;CAC3B,iBAAiB,mBAAmB;CACpC,MAAM,UAAU,MAAM;CACtB,iBAAiB;CACjB,OAAO,MAAM;AACf;AAEA,eAAsB,WAAW,KAAqC;CACpE,MAAM,EAAE,OAAO,MAAM,WAAW;CAChC,MAAM,OAAO,MAAM,OAAO,QAAQ;CAClC,MAAM,OAAyB;EAAE,GAAG;EAAK;CAAK;CAC9C,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY,MAAM;EAChE,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,uBAAuB;CACzD,MAAM,OAAQ,MAAM,SAAS,KAAK;CAElC,OAAO,sBAAsB,SAAS,WAAW,OAAO,IAAI,CAAC;CAC7D,MAAM,UAAU;CAChB,OAAO;AACT"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create visitor: ${response.status} ${await response.text()}`);\n }\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n try {\n cache.visitor = await visitorFetcher;\n return cache.visitor;\n } finally {\n // In a `finally`, so a rejected attempt is not left in `visitorFetcher` for every later\n // caller to await again: `sendEvents` needs a visitor for every batch, and one failed\n // request would otherwise stop the page from reporting anything until it is reloaded.\n visitorFetcher = null;\n }\n}\n\nexport async function setVisitor(dto: Omit<UpdateVisitorDTO, 'tags'>) {\n const { id } = await getVisitor();\n const tags = await config.getTags();\n const body: UpdateVisitorDTO = { ...dto, tags };\n const response = await fetch(`${config.endpoint}/visitors/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) throw new Error('Failed to set visitor');\n const data = (await response.json()) as Visitor;\n\n config.thirdPartyUserSetters.forEach((setter) => setter(body));\n cache.visitor = data;\n return data;\n}\n"],"mappings":";;;;AAMA,eAAe,gBAAkC;CAC/C,MAAM,OAAO,MAAM,OAAO,QAAQ;CAClC,MAAM,MAAwB;EAC5B,WAAW,MAAM,OAAO,YAAY;EACpC,UAAU,OAAO;EACjB,aAAa,OAAO;EACpB;EACA,YAAY;CACd;CAEA,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY;EAC1D,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC;CAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,6BAA6B,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;CAEzF,MAAM,OAAQ,MAAM,SAAS,KAAK;CAClC,IAAI,KAAK,IACP,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,EAAE;CAEjD,OAAO;AACT;AAEA,eAAe,qBAAuC;CACpD,MAAM,YAAY,OAAO,QAAQ,QAAQ,KAAK,UAAU;CACxD,IAAI,aAAa,cAAc,aAAa;EAS1C,MAAM,OAAO,MAAM,OAAO,QAAQ;EAClC,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY,aAAa;GACvE,QAAQ;GACR,aAAa;GACb,SAAS,MAAM,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,EAAE,KAAK,CAA4B;EAC1D,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,cAAc;EACvC,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,IACP,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,EAAE;EAEjD,OAAO;CACT,OACE,OAAO,cAAc;AAEzB;AAEA,IAAI,iBAA0C;AAE9C,eAAsB,aAA+B;CACnD,IAAI,MAAM,SAAS,OAAO,MAAM;CAChC,IAAI,gBAAgB,OAAO;CAC3B,iBAAiB,mBAAmB;CACpC,IAAI;EACF,MAAM,UAAU,MAAM;EACtB,OAAO,MAAM;CACf,UAAU;EAIR,iBAAiB;CACnB;AACF;AAEA,eAAsB,WAAW,KAAqC;CACpE,MAAM,EAAE,OAAO,MAAM,WAAW;CAChC,MAAM,OAAO,MAAM,OAAO,QAAQ;CAClC,MAAM,OAAyB;EAAE,GAAG;EAAK;CAAK;CAC9C,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY,MAAM;EAChE,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,uBAAuB;CACzD,MAAM,OAAQ,MAAM,SAAS,KAAK;CAElC,OAAO,sBAAsB,SAAS,WAAW,OAAO,IAAI,CAAC;CAC7D,MAAM,UAAU;CAChB,OAAO;AACT"}
@@ -9,10 +9,10 @@ let cookie = require("cookie");
9
9
  //#region src/web/index.ts
10
10
  var import_es5 = /* @__PURE__ */ require_runtime.__toESM(require_es5$1.default, 1);
11
11
  function getDeviceId() {
12
- const cached = localStorage.getItem(require_constants_storage.keys.device_id);
12
+ const cached = require_setup_index.config.storage.getItem(require_constants_storage.keys.device_id);
13
13
  if (cached) return cached;
14
14
  const id = crypto.randomUUID ? crypto.randomUUID() : (0, uuid.v4)();
15
- localStorage.setItem(require_constants_storage.keys.device_id, id);
15
+ require_setup_index.config.storage.setItem(require_constants_storage.keys.device_id, id);
16
16
  return id;
17
17
  }
18
18
  const links = /* @__PURE__ */ new Map();
@@ -24,7 +24,10 @@ const links = /* @__PURE__ */ new Map();
24
24
  function getCachedLink(id) {
25
25
  const cached = links.get(id);
26
26
  if (cached) return cached;
27
- const link = require_link_index.getLink(id);
27
+ const link = require_link_index.getLink(id).then((result) => {
28
+ if (!result) links.delete(id);
29
+ return result;
30
+ });
28
31
  links.set(id, link);
29
32
  return link;
30
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["keys","getLink","Bowser","config"],"sources":["../../src/web/index.ts"],"sourcesContent":["import Bowser from 'bowser';\nimport { parseCookie } from 'cookie';\nimport { v4 as uuidv4 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { type Link, getLink } from '../link/index';\nimport { type Storage, cache, config } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\nexport function getDeviceId() {\n const cached = localStorage.getItem(keys.device_id);\n if (cached) return cached;\n // lib.dom types `crypto.randomUUID` as always present, but it is missing in\n // insecure contexts and older browsers, so probe it before use.\n const id = (crypto as Partial<Crypto>).randomUUID ? crypto.randomUUID() : uuidv4();\n localStorage.setItem(keys.device_id, id);\n return id;\n}\n\nconst links = new Map<string, Promise<Link | null>>();\n\n/**\n * `getTags` runs once per event now, and the link a `?s=` id points at cannot change while the\n * page is open, so an uncached lookup would put an identical request on the wire for every\n * event the page sends.\n */\nfunction getCachedLink(id: string) {\n const cached = links.get(id);\n if (cached) return cached;\n const link = getLink(id);\n links.set(id, link);\n return link;\n}\n\n/** The user agent cannot change while the page is open, so it is parsed once. */\nlet parser: ReturnType<typeof Bowser.getParser> | undefined;\n\nexport async function getTags() {\n // Read the page before the first await: `getTags` runs when the event happens, and a single\n // page app can navigate while the link lookup below is still in flight.\n const page_location = window.location.href;\n const page_referrer = document.referrer || undefined;\n const page_title = document.title;\n\n parser ??= Bowser.getParser(window.navigator.userAgent);\n const params = new URLSearchParams(window.location.search);\n const os = parser.getOS();\n const browser = parser.getBrowser();\n const platform = parser.getPlatform();\n const parsed = parseCookie(document.cookie);\n\n const linkId = params.get('s');\n const link = linkId ? await getCachedLink(linkId) : null;\n\n const tags: TrackTags = {\n os: `${os.name} ${os.version}`,\n os_name: os.name,\n os_version: os.version,\n browser: `${browser.name} ${browser.version}`,\n browser_name: browser.name,\n browser_version: browser.version,\n device: platform.model,\n device_id: getDeviceId(),\n device_type: platform.type,\n device_vendor: platform.vendor,\n device_pixel_ratio: window.devicePixelRatio,\n screen_width: window.screen.width,\n screen_height: window.screen.height,\n screen_resolution: `${window.screen.width}x${window.screen.height}`,\n release: config.release,\n language: navigator.language,\n time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n page_location,\n page_referrer,\n page_title,\n // Meta Ads — _fbc is set server-side (see @shware/analytics/server resolveClickIdCookies)\n fbc: parsed._fbc ?? undefined,\n fbp: parsed._fbp,\n fbclid: params.get('fbclid') ?? undefined,\n ad_id: params.get('ad_id') ?? undefined,\n ad_name: params.get('ad_name') ?? undefined,\n adset_id: params.get('adset_id') ?? undefined,\n adset_name: params.get('adset_name') ?? undefined,\n campaign_id: params.get('campaign_id') ?? undefined,\n campaign_name: params.get('campaign_name') ?? undefined,\n placement: params.get('placement') ?? undefined,\n site_source_name: params.get('site_source_name') ?? undefined,\n // Google Ads\n gclid: params.get('gclid') ?? undefined,\n gclsrc: params.get('gclsrc') ?? undefined,\n gad_source: params.get('gad_source') ?? undefined,\n gad_campaignid: params.get('gad_campaignid') ?? undefined,\n // Reddit Ads — _rdt_cid is set server-side (see @shware/analytics/server resolveClickIdCookies)\n rdt_cid: params.get('rdt_cid') ?? parsed._rdt_cid ?? undefined,\n rdt_uuid: parsed._rdt_uuid,\n // LinkedIn Ads: get click id from url params or first-party cookie\n li_fat_id: params.get('li_fat_id') ?? parsed.li_fat_id ?? undefined,\n // click ids\n dclid: params.get('dclid') ?? undefined,\n ko_click_id: params.get('ko_click_id') ?? undefined,\n msclkid: params.get('msclkid') ?? undefined,\n sccid: params.get('sccid') ?? undefined,\n ttclid: params.get('ttclid') ?? undefined,\n twclid: params.get('twclid') ?? undefined,\n wbraid: params.get('wbraid') ?? undefined,\n yclid: params.get('yclid') ?? undefined,\n // utm params\n utm_source: link?.utm_source ?? params.get('utm_source') ?? undefined,\n utm_medium: link?.utm_medium ?? params.get('utm_medium') ?? undefined,\n utm_campaign: link?.utm_campaign ?? params.get('utm_campaign') ?? undefined,\n utm_term: link?.utm_term ?? params.get('utm_term') ?? undefined,\n utm_content: link?.utm_content ?? params.get('utm_content') ?? undefined,\n utm_id: link?.utm_id ?? params.get('utm_id') ?? undefined,\n utm_source_platform:\n link?.utm_source_platform ?? params.get('utm_source_platform') ?? undefined,\n utm_creative_format:\n link?.utm_creative_format ?? params.get('utm_creative_format') ?? undefined,\n utm_marketing_tactic:\n link?.utm_marketing_tactic ?? params.get('utm_marketing_tactic') ?? undefined,\n };\n\n cache.tags = tags;\n return tags;\n}\n\nconst map = new Map<string, string>();\n\nexport const storage: Storage = {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n console.error('localStorage is not available');\n return map.get(key) ?? null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n console.error('localStorage is not available');\n map.set(key, value);\n }\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["config","keys","getLink","Bowser"],"sources":["../../src/web/index.ts"],"sourcesContent":["import Bowser from 'bowser';\nimport { parseCookie } from 'cookie';\nimport { v4 as uuidv4 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { type Link, getLink } from '../link/index';\nimport { type Storage, cache, config } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\nexport function getDeviceId() {\n const cached = config.storage.getItem(keys.device_id);\n if (cached) return cached;\n // lib.dom types `crypto.randomUUID` as always present, but it is missing in\n // insecure contexts and older browsers, so probe it before use.\n const id = (crypto as Partial<Crypto>).randomUUID ? crypto.randomUUID() : uuidv4();\n config.storage.setItem(keys.device_id, id);\n return id;\n}\n\nconst links = new Map<string, Promise<Link | null>>();\n\n/**\n * `getTags` runs once per event now, and the link a `?s=` id points at cannot change while the\n * page is open, so an uncached lookup would put an identical request on the wire for every\n * event the page sends.\n */\nfunction getCachedLink(id: string) {\n const cached = links.get(id);\n if (cached) return cached;\n const link = getLink(id).then((result) => {\n // A lookup that came back empty must not stick for the lifetime of the page. `getLink`\n // answers null for a network failure as well as for a link that does not exist, and losing\n // the link's utm params for every later event costs more than repeating a rare request.\n if (!result) links.delete(id);\n return result;\n });\n links.set(id, link);\n return link;\n}\n\n/** The user agent cannot change while the page is open, so it is parsed once. */\nlet parser: ReturnType<typeof Bowser.getParser> | undefined;\n\nexport async function getTags() {\n // Read the page before the first await: `getTags` runs when the event happens, and a single\n // page app can navigate while the link lookup below is still in flight.\n const page_location = window.location.href;\n const page_referrer = document.referrer || undefined;\n const page_title = document.title;\n\n parser ??= Bowser.getParser(window.navigator.userAgent);\n const params = new URLSearchParams(window.location.search);\n const os = parser.getOS();\n const browser = parser.getBrowser();\n const platform = parser.getPlatform();\n const parsed = parseCookie(document.cookie);\n\n const linkId = params.get('s');\n const link = linkId ? await getCachedLink(linkId) : null;\n\n const tags: TrackTags = {\n os: `${os.name} ${os.version}`,\n os_name: os.name,\n os_version: os.version,\n browser: `${browser.name} ${browser.version}`,\n browser_name: browser.name,\n browser_version: browser.version,\n device: platform.model,\n device_id: getDeviceId(),\n device_type: platform.type,\n device_vendor: platform.vendor,\n device_pixel_ratio: window.devicePixelRatio,\n screen_width: window.screen.width,\n screen_height: window.screen.height,\n screen_resolution: `${window.screen.width}x${window.screen.height}`,\n release: config.release,\n language: navigator.language,\n time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n page_location,\n page_referrer,\n page_title,\n // Meta Ads — _fbc is set server-side (see @shware/analytics/server resolveClickIdCookies)\n fbc: parsed._fbc ?? undefined,\n fbp: parsed._fbp,\n fbclid: params.get('fbclid') ?? undefined,\n ad_id: params.get('ad_id') ?? undefined,\n ad_name: params.get('ad_name') ?? undefined,\n adset_id: params.get('adset_id') ?? undefined,\n adset_name: params.get('adset_name') ?? undefined,\n campaign_id: params.get('campaign_id') ?? undefined,\n campaign_name: params.get('campaign_name') ?? undefined,\n placement: params.get('placement') ?? undefined,\n site_source_name: params.get('site_source_name') ?? undefined,\n // Google Ads\n gclid: params.get('gclid') ?? undefined,\n gclsrc: params.get('gclsrc') ?? undefined,\n gad_source: params.get('gad_source') ?? undefined,\n gad_campaignid: params.get('gad_campaignid') ?? undefined,\n // Reddit Ads — _rdt_cid is set server-side (see @shware/analytics/server resolveClickIdCookies)\n rdt_cid: params.get('rdt_cid') ?? parsed._rdt_cid ?? undefined,\n rdt_uuid: parsed._rdt_uuid,\n // LinkedIn Ads: get click id from url params or first-party cookie\n li_fat_id: params.get('li_fat_id') ?? parsed.li_fat_id ?? undefined,\n // click ids\n dclid: params.get('dclid') ?? undefined,\n ko_click_id: params.get('ko_click_id') ?? undefined,\n msclkid: params.get('msclkid') ?? undefined,\n sccid: params.get('sccid') ?? undefined,\n ttclid: params.get('ttclid') ?? undefined,\n twclid: params.get('twclid') ?? undefined,\n wbraid: params.get('wbraid') ?? undefined,\n yclid: params.get('yclid') ?? undefined,\n // utm params\n utm_source: link?.utm_source ?? params.get('utm_source') ?? undefined,\n utm_medium: link?.utm_medium ?? params.get('utm_medium') ?? undefined,\n utm_campaign: link?.utm_campaign ?? params.get('utm_campaign') ?? undefined,\n utm_term: link?.utm_term ?? params.get('utm_term') ?? undefined,\n utm_content: link?.utm_content ?? params.get('utm_content') ?? undefined,\n utm_id: link?.utm_id ?? params.get('utm_id') ?? undefined,\n utm_source_platform:\n link?.utm_source_platform ?? params.get('utm_source_platform') ?? undefined,\n utm_creative_format:\n link?.utm_creative_format ?? params.get('utm_creative_format') ?? undefined,\n utm_marketing_tactic:\n link?.utm_marketing_tactic ?? params.get('utm_marketing_tactic') ?? undefined,\n };\n\n cache.tags = tags;\n return tags;\n}\n\nconst map = new Map<string, string>();\n\nexport const storage: Storage = {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n console.error('localStorage is not available');\n return map.get(key) ?? null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n console.error('localStorage is not available');\n map.set(key, value);\n }\n },\n};\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,cAAc;CAC5B,MAAM,SAASA,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,SAAS;CACpD,IAAI,QAAQ,OAAO;CAGnB,MAAM,KAAM,OAA2B,aAAa,OAAO,WAAW,KAAA,GAAA,KAAA,GAAA,CAAW;CACjF,oBAAA,OAAO,QAAQ,QAAQA,0BAAAA,KAAK,WAAW,EAAE;CACzC,OAAO;AACT;AAEA,MAAM,wBAAQ,IAAI,IAAkC;;;;;;AAOpD,SAAS,cAAc,IAAY;CACjC,MAAM,SAAS,MAAM,IAAI,EAAE;CAC3B,IAAI,QAAQ,OAAO;CACnB,MAAM,OAAOC,mBAAAA,QAAQ,EAAE,CAAC,CAAC,MAAM,WAAW;EAIxC,IAAI,CAAC,QAAQ,MAAM,OAAO,EAAE;EAC5B,OAAO;CACT,CAAC;CACD,MAAM,IAAI,IAAI,IAAI;CAClB,OAAO;AACT;;AAGA,IAAI;AAEJ,eAAsB,UAAU;CAG9B,MAAM,gBAAgB,OAAO,SAAS;CACtC,MAAM,gBAAgB,SAAS,YAAY,KAAA;CAC3C,MAAM,aAAa,SAAS;CAE5B,WAAWC,WAAAA,QAAO,UAAU,OAAO,UAAU,SAAS;CACtD,MAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;CACzD,MAAM,KAAK,OAAO,MAAM;CACxB,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,WAAW,OAAO,YAAY;CACpC,MAAM,UAAA,GAAA,OAAA,YAAA,CAAqB,SAAS,MAAM;CAE1C,MAAM,SAAS,OAAO,IAAI,GAAG;CAC7B,MAAM,OAAO,SAAS,MAAM,cAAc,MAAM,IAAI;CAEpD,MAAM,OAAkB;EACtB,IAAI,GAAG,GAAG,KAAK,GAAG,GAAG;EACrB,SAAS,GAAG;EACZ,YAAY,GAAG;EACf,SAAS,GAAG,QAAQ,KAAK,GAAG,QAAQ;EACpC,cAAc,QAAQ;EACtB,iBAAiB,QAAQ;EACzB,QAAQ,SAAS;EACjB,WAAW,YAAY;EACvB,aAAa,SAAS;EACtB,eAAe,SAAS;EACxB,oBAAoB,OAAO;EAC3B,cAAc,OAAO,OAAO;EAC5B,eAAe,OAAO,OAAO;EAC7B,mBAAmB,GAAG,OAAO,OAAO,MAAM,GAAG,OAAO,OAAO;EAC3D,SAASH,oBAAAA,OAAO;EAChB,UAAU,UAAU;EACpB,WAAW,KAAK,eAAe,CAAC,CAAC,gBAAgB,CAAC,CAAC;EACnD;EACA;EACA;EAEA,KAAK,OAAO,QAAQ,KAAA;EACpB,KAAK,OAAO;EACZ,QAAQ,OAAO,IAAI,QAAQ,KAAK,KAAA;EAChC,OAAO,OAAO,IAAI,OAAO,KAAK,KAAA;EAC9B,SAAS,OAAO,IAAI,SAAS,KAAK,KAAA;EAClC,UAAU,OAAO,IAAI,UAAU,KAAK,KAAA;EACpC,YAAY,OAAO,IAAI,YAAY,KAAK,KAAA;EACxC,aAAa,OAAO,IAAI,aAAa,KAAK,KAAA;EAC1C,eAAe,OAAO,IAAI,eAAe,KAAK,KAAA;EAC9C,WAAW,OAAO,IAAI,WAAW,KAAK,KAAA;EACtC,kBAAkB,OAAO,IAAI,kBAAkB,KAAK,KAAA;EAEpD,OAAO,OAAO,IAAI,OAAO,KAAK,KAAA;EAC9B,QAAQ,OAAO,IAAI,QAAQ,KAAK,KAAA;EAChC,YAAY,OAAO,IAAI,YAAY,KAAK,KAAA;EACxC,gBAAgB,OAAO,IAAI,gBAAgB,KAAK,KAAA;EAEhD,SAAS,OAAO,IAAI,SAAS,KAAK,OAAO,YAAY,KAAA;EACrD,UAAU,OAAO;EAEjB,WAAW,OAAO,IAAI,WAAW,KAAK,OAAO,aAAa,KAAA;EAE1D,OAAO,OAAO,IAAI,OAAO,KAAK,KAAA;EAC9B,aAAa,OAAO,IAAI,aAAa,KAAK,KAAA;EAC1C,SAAS,OAAO,IAAI,SAAS,KAAK,KAAA;EAClC,OAAO,OAAO,IAAI,OAAO,KAAK,KAAA;EAC9B,QAAQ,OAAO,IAAI,QAAQ,KAAK,KAAA;EAChC,QAAQ,OAAO,IAAI,QAAQ,KAAK,KAAA;EAChC,QAAQ,OAAO,IAAI,QAAQ,KAAK,KAAA;EAChC,OAAO,OAAO,IAAI,OAAO,KAAK,KAAA;EAE9B,YAAY,MAAM,cAAc,OAAO,IAAI,YAAY,KAAK,KAAA;EAC5D,YAAY,MAAM,cAAc,OAAO,IAAI,YAAY,KAAK,KAAA;EAC5D,cAAc,MAAM,gBAAgB,OAAO,IAAI,cAAc,KAAK,KAAA;EAClE,UAAU,MAAM,YAAY,OAAO,IAAI,UAAU,KAAK,KAAA;EACtD,aAAa,MAAM,eAAe,OAAO,IAAI,aAAa,KAAK,KAAA;EAC/D,QAAQ,MAAM,UAAU,OAAO,IAAI,QAAQ,KAAK,KAAA;EAChD,qBACE,MAAM,uBAAuB,OAAO,IAAI,qBAAqB,KAAK,KAAA;EACpE,qBACE,MAAM,uBAAuB,OAAO,IAAI,qBAAqB,KAAK,KAAA;EACpE,sBACE,MAAM,wBAAwB,OAAO,IAAI,sBAAsB,KAAK,KAAA;CACxE;CAEA,oBAAA,MAAM,OAAO;CACb,OAAO;AACT;AAEA,MAAM,sBAAM,IAAI,IAAoB;AAEpC,MAAa,UAAmB;CAC9B,UAAU,QAAQ;EAChB,IAAI;GACF,OAAO,aAAa,QAAQ,GAAG;EACjC,QAAQ;GACN,QAAQ,MAAM,+BAA+B;GAC7C,OAAO,IAAI,IAAI,GAAG,KAAK;EACzB;CACF;CACA,UAAU,KAAK,UAAU;EACvB,IAAI;GACF,aAAa,QAAQ,KAAK,KAAK;EACjC,QAAQ;GACN,QAAQ,MAAM,+BAA+B;GAC7C,IAAI,IAAI,KAAK,KAAK;EACpB;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/web/index.ts"],"mappings":";;;iBAQgB;iBA4BM,WAAO,QAAA;cA0FhB,SAAS"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/web/index.ts"],"mappings":";;;iBAQgB;iBAkCM,WAAO,QAAA;cA0FhB,SAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/web/index.ts"],"mappings":";;;iBAQgB;iBA4BM,WAAO,QAAA;cA0FhB,SAAS"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/web/index.ts"],"mappings":";;;iBAQgB;iBAkCM,WAAO,QAAA;cA0FhB,SAAS"}
@@ -8,10 +8,10 @@ import { parseCookie } from "cookie";
8
8
  //#region src/web/index.ts
9
9
  var import_es5 = /* @__PURE__ */ __toESM(require_es5(), 1);
10
10
  function getDeviceId() {
11
- const cached = localStorage.getItem(keys.device_id);
11
+ const cached = config.storage.getItem(keys.device_id);
12
12
  if (cached) return cached;
13
13
  const id = crypto.randomUUID ? crypto.randomUUID() : v4();
14
- localStorage.setItem(keys.device_id, id);
14
+ config.storage.setItem(keys.device_id, id);
15
15
  return id;
16
16
  }
17
17
  const links = /* @__PURE__ */ new Map();
@@ -23,7 +23,10 @@ const links = /* @__PURE__ */ new Map();
23
23
  function getCachedLink(id) {
24
24
  const cached = links.get(id);
25
25
  if (cached) return cached;
26
- const link = getLink(id);
26
+ const link = getLink(id).then((result) => {
27
+ if (!result) links.delete(id);
28
+ return result;
29
+ });
27
30
  links.set(id, link);
28
31
  return link;
29
32
  }