@shware/analytics 2.16.1 → 2.17.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 (61) hide show
  1. package/dist/feedback/index.cjs +2 -2
  2. package/dist/feedback/index.cjs.map +1 -1
  3. package/dist/feedback/index.mjs +1 -1
  4. package/dist/feedback/index.mjs.map +1 -1
  5. package/dist/hooks/use-click-id-persistence.cjs +3 -3
  6. package/dist/hooks/use-click-id-persistence.cjs.map +1 -1
  7. package/dist/hooks/use-click-id-persistence.mjs +1 -1
  8. package/dist/hooks/use-click-id-persistence.mjs.map +1 -1
  9. package/dist/hooks/use-web-session-analytics.cjs +18 -11
  10. package/dist/hooks/use-web-session-analytics.cjs.map +1 -1
  11. package/dist/hooks/use-web-session-analytics.mjs +18 -11
  12. package/dist/hooks/use-web-session-analytics.mjs.map +1 -1
  13. package/dist/link/index.cjs +3 -3
  14. package/dist/link/index.cjs.map +1 -1
  15. package/dist/link/index.mjs +1 -1
  16. package/dist/link/index.mjs.map +1 -1
  17. package/dist/server/linkedin-conversions-api.cjs +2 -2
  18. package/dist/server/linkedin-conversions-api.cjs.map +1 -1
  19. package/dist/server/linkedin-conversions-api.mjs +1 -1
  20. package/dist/server/linkedin-conversions-api.mjs.map +1 -1
  21. package/dist/server/reddit-conversions-api.cjs +2 -2
  22. package/dist/server/reddit-conversions-api.cjs.map +1 -1
  23. package/dist/server/reddit-conversions-api.mjs +1 -1
  24. package/dist/server/reddit-conversions-api.mjs.map +1 -1
  25. package/dist/setup/session.cjs +2 -0
  26. package/dist/setup/session.cjs.map +1 -1
  27. package/dist/setup/session.d.cts +2 -1
  28. package/dist/setup/session.d.ts +2 -1
  29. package/dist/setup/session.mjs +1 -0
  30. package/dist/setup/session.mjs.map +1 -1
  31. package/dist/track/index.cjs +3 -4
  32. package/dist/track/index.cjs.map +1 -1
  33. package/dist/track/index.mjs +1 -2
  34. package/dist/track/index.mjs.map +1 -1
  35. package/dist/visitor/index.cjs +4 -4
  36. package/dist/visitor/index.cjs.map +1 -1
  37. package/dist/visitor/index.mjs +1 -1
  38. package/dist/visitor/index.mjs.map +1 -1
  39. package/dist/web/index.cjs +3 -3
  40. package/dist/web/index.cjs.map +1 -1
  41. package/dist/web/index.mjs +1 -1
  42. package/dist/web/index.mjs.map +1 -1
  43. package/package.json +3 -2
  44. package/dist/utils/fetch.cjs +0 -78
  45. package/dist/utils/fetch.cjs.map +0 -1
  46. package/dist/utils/fetch.d.cts +0 -16
  47. package/dist/utils/fetch.d.ts +0 -16
  48. package/dist/utils/fetch.mjs +0 -53
  49. package/dist/utils/fetch.mjs.map +0 -1
  50. package/dist/utils/storage.cjs +0 -56
  51. package/dist/utils/storage.cjs.map +0 -1
  52. package/dist/utils/storage.d.cts +0 -10
  53. package/dist/utils/storage.d.ts +0 -10
  54. package/dist/utils/storage.mjs +0 -31
  55. package/dist/utils/storage.mjs.map +0 -1
  56. package/dist/utils/token-bucket.cjs +0 -73
  57. package/dist/utils/token-bucket.cjs.map +0 -1
  58. package/dist/utils/token-bucket.d.cts +0 -20
  59. package/dist/utils/token-bucket.d.ts +0 -20
  60. package/dist/utils/token-bucket.mjs +0 -48
  61. package/dist/utils/token-bucket.mjs.map +0 -1
@@ -23,10 +23,10 @@ __export(feedback_exports, {
23
23
  sendFeedback: () => sendFeedback
24
24
  });
25
25
  module.exports = __toCommonJS(feedback_exports);
26
+ var import_utils = require("@shware/utils");
26
27
  var import_setup = require("../setup/index.cjs");
27
- var import_fetch = require("../utils/fetch.cjs");
28
28
  async function sendFeedback(dto) {
29
- const response = await (0, import_fetch.fetch)(`${import_setup.config.endpoint}/feedback`, {
29
+ const response = await (0, import_utils.fetch)(`${import_setup.config.endpoint}/feedback`, {
30
30
  method: "POST",
31
31
  credentials: "include",
32
32
  headers: await import_setup.config.getHeaders(),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/index.ts"],"sourcesContent":["import { config } from '../setup/index';\nimport { fetch } from '../utils/fetch';\nimport type { CreateFeedbackDTO } from '../schema/index';\n\nexport async function sendFeedback(dto: CreateFeedbackDTO) {\n const response = await fetch(`${config.endpoint}/feedback`, {\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 feedback: ${response.status} ${await response.text()}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAuB;AACvB,mBAAsB;AAGtB,eAAsB,aAAa,KAAwB;AACzD,QAAM,WAAW,UAAM,oBAAM,GAAG,oBAAO,QAAQ,aAAa;AAAA,IAC1D,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,MAAM,oBAAO,WAAW;AAAA,IACjC,MAAM,KAAK,UAAU,GAAG;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,4BAA4B,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AAAA,EACxF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { config } from '../setup/index';\nimport type { CreateFeedbackDTO } from '../schema/index';\n\nexport async function sendFeedback(dto: CreateFeedbackDTO) {\n const response = await fetch(`${config.endpoint}/feedback`, {\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 feedback: ${response.status} ${await response.text()}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAsB;AACtB,mBAAuB;AAGvB,eAAsB,aAAa,KAAwB;AACzD,QAAM,WAAW,UAAM,oBAAM,GAAG,oBAAO,QAAQ,aAAa;AAAA,IAC1D,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,MAAM,oBAAO,WAAW;AAAA,IACjC,MAAM,KAAK,UAAU,GAAG;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,4BAA4B,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AAAA,EACxF;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  // src/feedback/index.ts
2
+ import { fetch } from "@shware/utils";
2
3
  import { config } from "../setup/index.mjs";
3
- import { fetch } from "../utils/fetch.mjs";
4
4
  async function sendFeedback(dto) {
5
5
  const response = await fetch(`${config.endpoint}/feedback`, {
6
6
  method: "POST",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/index.ts"],"sourcesContent":["import { config } from '../setup/index';\nimport { fetch } from '../utils/fetch';\nimport type { CreateFeedbackDTO } from '../schema/index';\n\nexport async function sendFeedback(dto: CreateFeedbackDTO) {\n const response = await fetch(`${config.endpoint}/feedback`, {\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 feedback: ${response.status} ${await response.text()}`);\n }\n}\n"],"mappings":";AAAA,SAAS,cAAc;AACvB,SAAS,aAAa;AAGtB,eAAsB,aAAa,KAAwB;AACzD,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,QAAQ,aAAa;AAAA,IAC1D,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,MAAM,OAAO,WAAW;AAAA,IACjC,MAAM,KAAK,UAAU,GAAG;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,4BAA4B,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AAAA,EACxF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { config } from '../setup/index';\nimport type { CreateFeedbackDTO } from '../schema/index';\n\nexport async function sendFeedback(dto: CreateFeedbackDTO) {\n const response = await fetch(`${config.endpoint}/feedback`, {\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 feedback: ${response.status} ${await response.text()}`);\n }\n}\n"],"mappings":";AAAA,SAAS,aAAa;AACtB,SAAS,cAAc;AAGvB,eAAsB,aAAa,KAAwB;AACzD,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,QAAQ,aAAa;AAAA,IAC1D,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,MAAM,OAAO,WAAW;AAAA,IACjC,MAAM,KAAK,UAAU,GAAG;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,4BAA4B,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AAAA,EACxF;AACF;","names":[]}
@@ -23,8 +23,8 @@ __export(use_click_id_persistence_exports, {
23
23
  useClickIdPersistence: () => useClickIdPersistence
24
24
  });
25
25
  module.exports = __toCommonJS(use_click_id_persistence_exports);
26
+ var import_utils = require("@shware/utils");
26
27
  var import_react = require("react");
27
- var import_storage = require("../utils/storage.cjs");
28
28
  function setCookie(name, value, ttlInMs) {
29
29
  const d = /* @__PURE__ */ new Date();
30
30
  d.setTime(d.getTime() + ttlInMs);
@@ -40,11 +40,11 @@ function useClickIdPersistence() {
40
40
  if (fbclid) {
41
41
  const fbc = `fb.1.${Date.now()}.${fbclid}`;
42
42
  setCookie("_fbc", fbc, ttlMs);
43
- import_storage.expiringStorage.setItem("fbc", fbc, ttlMs);
43
+ import_utils.expiringStorage.setItem("fbc", fbc, ttlMs);
44
44
  }
45
45
  if (rdt_cid) {
46
46
  setCookie("_rdt_cid", rdt_cid, ttlMs);
47
- import_storage.expiringStorage.setItem("rdt_cid", rdt_cid, ttlMs);
47
+ import_utils.expiringStorage.setItem("rdt_cid", rdt_cid, ttlMs);
48
48
  }
49
49
  }, []);
50
50
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/use-click-id-persistence.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { expiringStorage } from '../utils/storage';\n\nfunction setCookie(name: string, value: string, ttlInMs: number) {\n const d = new Date();\n d.setTime(d.getTime() + ttlInMs);\n const expires = 'expires=' + d.toUTCString();\n document.cookie = `${name}=${value}; ${expires}; path=/; SameSite=Lax; Secure`;\n}\n\n// todo: do not set tracking cookies before the user has granted consent where required.\n// reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc/#3--store-clickid\n// reference: https://watsspace.com/blog/meta-conversions-api-fbc-and-fbp-parameters/\nexport function useClickIdPersistence() {\n useEffect(() => {\n const params = new URLSearchParams(window.location.search);\n const fbclid = params.get('fbclid');\n const rdt_cid = params.get('rdt_cid');\n\n // common practice ~90 days\n const ttlMs = 90 * 24 * 60 * 60 * 1000;\n\n if (fbclid) {\n const fbc = `fb.1.${Date.now()}.${fbclid}`;\n setCookie('_fbc', fbc, ttlMs);\n expiringStorage.setItem('fbc', fbc, ttlMs);\n }\n\n if (rdt_cid) {\n setCookie('_rdt_cid', rdt_cid, ttlMs);\n expiringStorage.setItem('rdt_cid', rdt_cid, ttlMs);\n }\n }, []);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA0B;AAC1B,qBAAgC;AAEhC,SAAS,UAAU,MAAc,OAAe,SAAiB;AAC/D,QAAM,IAAI,oBAAI,KAAK;AACnB,IAAE,QAAQ,EAAE,QAAQ,IAAI,OAAO;AAC/B,QAAM,UAAU,aAAa,EAAE,YAAY;AAC3C,WAAS,SAAS,GAAG,IAAI,IAAI,KAAK,KAAK,OAAO;AAChD;AAKO,SAAS,wBAAwB;AACtC,8BAAU,MAAM;AACd,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,UAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,UAAM,UAAU,OAAO,IAAI,SAAS;AAGpC,UAAM,QAAQ,KAAK,KAAK,KAAK,KAAK;AAElC,QAAI,QAAQ;AACV,YAAM,MAAM,QAAQ,KAAK,IAAI,CAAC,IAAI,MAAM;AACxC,gBAAU,QAAQ,KAAK,KAAK;AAC5B,qCAAgB,QAAQ,OAAO,KAAK,KAAK;AAAA,IAC3C;AAEA,QAAI,SAAS;AACX,gBAAU,YAAY,SAAS,KAAK;AACpC,qCAAgB,QAAQ,WAAW,SAAS,KAAK;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,CAAC;AACP;","names":[]}
1
+ {"version":3,"sources":["../../src/hooks/use-click-id-persistence.ts"],"sourcesContent":["import { expiringStorage } from '@shware/utils';\nimport { useEffect } from 'react';\n\nfunction setCookie(name: string, value: string, ttlInMs: number) {\n const d = new Date();\n d.setTime(d.getTime() + ttlInMs);\n const expires = 'expires=' + d.toUTCString();\n document.cookie = `${name}=${value}; ${expires}; path=/; SameSite=Lax; Secure`;\n}\n\n// todo: do not set tracking cookies before the user has granted consent where required.\n// reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc/#3--store-clickid\n// reference: https://watsspace.com/blog/meta-conversions-api-fbc-and-fbp-parameters/\nexport function useClickIdPersistence() {\n useEffect(() => {\n const params = new URLSearchParams(window.location.search);\n const fbclid = params.get('fbclid');\n const rdt_cid = params.get('rdt_cid');\n\n // common practice ~90 days\n const ttlMs = 90 * 24 * 60 * 60 * 1000;\n\n if (fbclid) {\n const fbc = `fb.1.${Date.now()}.${fbclid}`;\n setCookie('_fbc', fbc, ttlMs);\n expiringStorage.setItem('fbc', fbc, ttlMs);\n }\n\n if (rdt_cid) {\n setCookie('_rdt_cid', rdt_cid, ttlMs);\n expiringStorage.setItem('rdt_cid', rdt_cid, ttlMs);\n }\n }, []);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAgC;AAChC,mBAA0B;AAE1B,SAAS,UAAU,MAAc,OAAe,SAAiB;AAC/D,QAAM,IAAI,oBAAI,KAAK;AACnB,IAAE,QAAQ,EAAE,QAAQ,IAAI,OAAO;AAC/B,QAAM,UAAU,aAAa,EAAE,YAAY;AAC3C,WAAS,SAAS,GAAG,IAAI,IAAI,KAAK,KAAK,OAAO;AAChD;AAKO,SAAS,wBAAwB;AACtC,8BAAU,MAAM;AACd,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,UAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,UAAM,UAAU,OAAO,IAAI,SAAS;AAGpC,UAAM,QAAQ,KAAK,KAAK,KAAK,KAAK;AAElC,QAAI,QAAQ;AACV,YAAM,MAAM,QAAQ,KAAK,IAAI,CAAC,IAAI,MAAM;AACxC,gBAAU,QAAQ,KAAK,KAAK;AAC5B,mCAAgB,QAAQ,OAAO,KAAK,KAAK;AAAA,IAC3C;AAEA,QAAI,SAAS;AACX,gBAAU,YAAY,SAAS,KAAK;AACpC,mCAAgB,QAAQ,WAAW,SAAS,KAAK;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,CAAC;AACP;","names":[]}
@@ -1,6 +1,6 @@
1
1
  // src/hooks/use-click-id-persistence.ts
2
+ import { expiringStorage } from "@shware/utils";
2
3
  import { useEffect } from "react";
3
- import { expiringStorage } from "../utils/storage.mjs";
4
4
  function setCookie(name, value, ttlInMs) {
5
5
  const d = /* @__PURE__ */ new Date();
6
6
  d.setTime(d.getTime() + ttlInMs);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/use-click-id-persistence.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { expiringStorage } from '../utils/storage';\n\nfunction setCookie(name: string, value: string, ttlInMs: number) {\n const d = new Date();\n d.setTime(d.getTime() + ttlInMs);\n const expires = 'expires=' + d.toUTCString();\n document.cookie = `${name}=${value}; ${expires}; path=/; SameSite=Lax; Secure`;\n}\n\n// todo: do not set tracking cookies before the user has granted consent where required.\n// reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc/#3--store-clickid\n// reference: https://watsspace.com/blog/meta-conversions-api-fbc-and-fbp-parameters/\nexport function useClickIdPersistence() {\n useEffect(() => {\n const params = new URLSearchParams(window.location.search);\n const fbclid = params.get('fbclid');\n const rdt_cid = params.get('rdt_cid');\n\n // common practice ~90 days\n const ttlMs = 90 * 24 * 60 * 60 * 1000;\n\n if (fbclid) {\n const fbc = `fb.1.${Date.now()}.${fbclid}`;\n setCookie('_fbc', fbc, ttlMs);\n expiringStorage.setItem('fbc', fbc, ttlMs);\n }\n\n if (rdt_cid) {\n setCookie('_rdt_cid', rdt_cid, ttlMs);\n expiringStorage.setItem('rdt_cid', rdt_cid, ttlMs);\n }\n }, []);\n}\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAEhC,SAAS,UAAU,MAAc,OAAe,SAAiB;AAC/D,QAAM,IAAI,oBAAI,KAAK;AACnB,IAAE,QAAQ,EAAE,QAAQ,IAAI,OAAO;AAC/B,QAAM,UAAU,aAAa,EAAE,YAAY;AAC3C,WAAS,SAAS,GAAG,IAAI,IAAI,KAAK,KAAK,OAAO;AAChD;AAKO,SAAS,wBAAwB;AACtC,YAAU,MAAM;AACd,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,UAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,UAAM,UAAU,OAAO,IAAI,SAAS;AAGpC,UAAM,QAAQ,KAAK,KAAK,KAAK,KAAK;AAElC,QAAI,QAAQ;AACV,YAAM,MAAM,QAAQ,KAAK,IAAI,CAAC,IAAI,MAAM;AACxC,gBAAU,QAAQ,KAAK,KAAK;AAC5B,sBAAgB,QAAQ,OAAO,KAAK,KAAK;AAAA,IAC3C;AAEA,QAAI,SAAS;AACX,gBAAU,YAAY,SAAS,KAAK;AACpC,sBAAgB,QAAQ,WAAW,SAAS,KAAK;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,CAAC;AACP;","names":[]}
1
+ {"version":3,"sources":["../../src/hooks/use-click-id-persistence.ts"],"sourcesContent":["import { expiringStorage } from '@shware/utils';\nimport { useEffect } from 'react';\n\nfunction setCookie(name: string, value: string, ttlInMs: number) {\n const d = new Date();\n d.setTime(d.getTime() + ttlInMs);\n const expires = 'expires=' + d.toUTCString();\n document.cookie = `${name}=${value}; ${expires}; path=/; SameSite=Lax; Secure`;\n}\n\n// todo: do not set tracking cookies before the user has granted consent where required.\n// reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc/#3--store-clickid\n// reference: https://watsspace.com/blog/meta-conversions-api-fbc-and-fbp-parameters/\nexport function useClickIdPersistence() {\n useEffect(() => {\n const params = new URLSearchParams(window.location.search);\n const fbclid = params.get('fbclid');\n const rdt_cid = params.get('rdt_cid');\n\n // common practice ~90 days\n const ttlMs = 90 * 24 * 60 * 60 * 1000;\n\n if (fbclid) {\n const fbc = `fb.1.${Date.now()}.${fbclid}`;\n setCookie('_fbc', fbc, ttlMs);\n expiringStorage.setItem('fbc', fbc, ttlMs);\n }\n\n if (rdt_cid) {\n setCookie('_rdt_cid', rdt_cid, ttlMs);\n expiringStorage.setItem('rdt_cid', rdt_cid, ttlMs);\n }\n }, []);\n}\n"],"mappings":";AAAA,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAE1B,SAAS,UAAU,MAAc,OAAe,SAAiB;AAC/D,QAAM,IAAI,oBAAI,KAAK;AACnB,IAAE,QAAQ,EAAE,QAAQ,IAAI,OAAO;AAC/B,QAAM,UAAU,aAAa,EAAE,YAAY;AAC3C,WAAS,SAAS,GAAG,IAAI,IAAI,KAAK,KAAK,OAAO;AAChD;AAKO,SAAS,wBAAwB;AACtC,YAAU,MAAM;AACd,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,UAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,UAAM,UAAU,OAAO,IAAI,SAAS;AAGpC,UAAM,QAAQ,KAAK,KAAK,KAAK,KAAK;AAElC,QAAI,QAAQ;AACV,YAAM,MAAM,QAAQ,KAAK,IAAI,CAAC,IAAI,MAAM;AACxC,gBAAU,QAAQ,KAAK,KAAK;AAC5B,sBAAgB,QAAQ,OAAO,KAAK,KAAK;AAAA,IAC3C;AAEA,QAAI,SAAS;AACX,gBAAU,YAAY,SAAS,KAAK;AACpC,sBAAgB,QAAQ,WAAW,SAAS,KAAK;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,CAAC;AACP;","names":[]}
@@ -23,10 +23,10 @@ __export(use_web_session_analytics_exports, {
23
23
  useWebSessionAnalytics: () => useWebSessionAnalytics
24
24
  });
25
25
  module.exports = __toCommonJS(use_web_session_analytics_exports);
26
+ var import_utils = require("@shware/utils");
26
27
  var import_react = require("react");
28
+ var import_session = require("../setup/session.cjs");
27
29
  var import_track = require("../track/index.cjs");
28
- var scrollGap = 500;
29
- var scrollThreshold = 0.9;
30
30
  function useWebSessionAnalytics(pathname) {
31
31
  const isActive = (0, import_react.useRef)(true);
32
32
  const isFocused = (0, import_react.useRef)(true);
@@ -34,12 +34,11 @@ function useWebSessionAnalytics(pathname) {
34
34
  const startTime = (0, import_react.useRef)(Date.now());
35
35
  const accumulatedTime = (0, import_react.useRef)(0);
36
36
  const hasSendScroll = (0, import_react.useRef)(false);
37
- const lastScrollTime = (0, import_react.useRef)(0);
38
37
  const updateAccumulator = (0, import_react.useCallback)(() => {
39
38
  const now = Date.now();
40
39
  if (isFocused.current && isVisible.current && isActive.current) {
41
40
  const delta = now - startTime.current;
42
- if (delta > 0) {
41
+ if (delta > 0 && delta < import_session.SESSION_TIMEOUT) {
43
42
  accumulatedTime.current += delta;
44
43
  }
45
44
  }
@@ -66,6 +65,7 @@ function useWebSessionAnalytics(pathname) {
66
65
  isFocused.current = typeof document !== "undefined" && document.hasFocus();
67
66
  isVisible.current = typeof document !== "undefined" && document.visibilityState === "visible";
68
67
  startTime.current = Date.now();
68
+ (0, import_track.track)("session_start", {}, { enableThirdPartyTracking: false });
69
69
  const onFocus = () => {
70
70
  updateAccumulator();
71
71
  isFocused.current = true;
@@ -92,27 +92,29 @@ function useWebSessionAnalytics(pathname) {
92
92
  sendUserEngagement();
93
93
  }
94
94
  };
95
- const onScroll = () => {
96
- const now = Date.now();
97
- if (now - lastScrollTime.current < scrollGap) return;
98
- lastScrollTime.current = now;
95
+ const onScroll = (0, import_utils.throttle)(() => {
96
+ updateAccumulator();
99
97
  if (hasSendScroll.current) return;
100
98
  const scrollTop = window.scrollY || document.documentElement.scrollTop;
101
99
  const windowHeight = window.innerHeight;
102
100
  const docHeight = document.documentElement.scrollHeight;
103
101
  if (docHeight === 0) return;
104
102
  const scrollPercent = (scrollTop + windowHeight) / docHeight;
105
- if (scrollPercent < scrollThreshold) return;
103
+ if (scrollPercent < 0.9) return;
106
104
  hasSendScroll.current = true;
107
105
  sendScroll();
108
- };
109
- (0, import_track.track)("session_start", {}, { enableThirdPartyTracking: false });
106
+ }, 500);
107
+ const checkpointEvents = ["mousedown", "keydown", "touchstart"];
108
+ const checkpoint = (0, import_utils.throttle)(updateAccumulator, 1e3);
110
109
  window.addEventListener("focus", onFocus, { passive: true });
111
110
  window.addEventListener("blur", onBlur, { passive: true });
112
111
  window.addEventListener("pageshow", onPageShow, { passive: true });
113
112
  window.addEventListener("pagehide", onPageHide, { passive: true });
114
113
  window.addEventListener("scroll", onScroll, { passive: true });
115
114
  document.addEventListener("visibilitychange", onVisibilityChange, { passive: true });
115
+ checkpointEvents.forEach((event) => {
116
+ window.addEventListener(event, checkpoint, { passive: true, capture: true });
117
+ });
116
118
  return () => {
117
119
  window.removeEventListener("focus", onFocus);
118
120
  window.removeEventListener("blur", onBlur);
@@ -120,6 +122,11 @@ function useWebSessionAnalytics(pathname) {
120
122
  window.removeEventListener("pagehide", onPageHide);
121
123
  window.removeEventListener("scroll", onScroll);
122
124
  document.removeEventListener("visibilitychange", onVisibilityChange);
125
+ checkpointEvents.forEach((event) => {
126
+ window.removeEventListener(event, checkpoint);
127
+ });
128
+ onScroll.cancel();
129
+ checkpoint.cancel();
123
130
  };
124
131
  }, []);
125
132
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/use-web-session-analytics.ts"],"sourcesContent":["/**\n * reference:\n * - Discover how long someone spends engaged on your website or app in Google Analytics](https://support.google.com/analytics/answer/11109416?hl=en)\n */\nimport { useCallback, useEffect, useRef } from 'react';\nimport { sendBeacon, track } from '../track/index';\n\nconst scrollGap = 500;\nconst scrollThreshold = 0.9;\n\n/**\n * 1. send session_start event when the page is loaded\n * 2. send scroll event when the user scrolls more than 90% of the page\n * 3. send user_engagement event when the page is hidden or the user is not focused\n */\nexport function useWebSessionAnalytics(pathname: string) {\n const isActive = useRef(true);\n const isFocused = useRef(true);\n const isVisible = useRef(true);\n\n const startTime = useRef(Date.now());\n const accumulatedTime = useRef(0);\n\n const hasSendScroll = useRef(false);\n const lastScrollTime = useRef(0);\n\n const updateAccumulator = useCallback(() => {\n const now = Date.now();\n if (isFocused.current && isVisible.current && isActive.current) {\n const delta = now - startTime.current;\n if (delta > 0) {\n accumulatedTime.current += delta;\n }\n }\n startTime.current = now;\n }, []);\n\n const sendUserEngagement = useCallback(() => {\n updateAccumulator();\n const engagement_time_msec = accumulatedTime.current;\n accumulatedTime.current = 0;\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec });\n }, [updateAccumulator]);\n\n const sendScroll = useCallback(() => {\n updateAccumulator();\n const engagement_time_msec = accumulatedTime.current;\n accumulatedTime.current = 0;\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec }, { enableThirdPartyTracking: false });\n }, [updateAccumulator]);\n\n // reset scroll state when the pathname changes, so we can send scroll when the user navigates to\n // a new page\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n isFocused.current = typeof document !== 'undefined' && document.hasFocus();\n isVisible.current = typeof document !== 'undefined' && document.visibilityState === 'visible';\n startTime.current = Date.now();\n\n const onFocus = () => {\n updateAccumulator();\n isFocused.current = true;\n };\n\n const onBlur = () => {\n updateAccumulator();\n isFocused.current = false;\n };\n\n const onPageShow = () => {\n updateAccumulator();\n isActive.current = true;\n };\n\n const onPageHide = () => {\n updateAccumulator();\n isActive.current = false;\n sendUserEngagement();\n };\n\n const onVisibilityChange = () => {\n updateAccumulator();\n if (document.visibilityState === 'visible') {\n isVisible.current = true;\n } else {\n isVisible.current = false;\n sendUserEngagement();\n }\n };\n\n const onScroll = () => {\n const now = Date.now();\n if (now - lastScrollTime.current < scrollGap) return;\n lastScrollTime.current = now;\n if (hasSendScroll.current) return;\n\n // only send scroll when the user has scrolled more than 90% of the page\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return;\n const scrollPercent = (scrollTop + windowHeight) / docHeight;\n if (scrollPercent < scrollThreshold) return;\n hasSendScroll.current = true;\n\n sendScroll();\n };\n\n track('session_start', {}, { enableThirdPartyTracking: false });\n\n window.addEventListener('focus', onFocus, { passive: true });\n window.addEventListener('blur', onBlur, { passive: true });\n window.addEventListener('pageshow', onPageShow, { passive: true });\n window.addEventListener('pagehide', onPageHide, { passive: true });\n window.addEventListener('scroll', onScroll, { passive: true });\n document.addEventListener('visibilitychange', onVisibilityChange, { passive: true });\n\n return () => {\n window.removeEventListener('focus', onFocus);\n window.removeEventListener('blur', onBlur);\n window.removeEventListener('pageshow', onPageShow);\n window.removeEventListener('pagehide', onPageHide);\n window.removeEventListener('scroll', onScroll);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n };\n }, []);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,mBAA+C;AAC/C,mBAAkC;AAElC,IAAM,YAAY;AAClB,IAAM,kBAAkB;AAOjB,SAAS,uBAAuB,UAAkB;AACvD,QAAM,eAAW,qBAAO,IAAI;AAC5B,QAAM,gBAAY,qBAAO,IAAI;AAC7B,QAAM,gBAAY,qBAAO,IAAI;AAE7B,QAAM,gBAAY,qBAAO,KAAK,IAAI,CAAC;AACnC,QAAM,sBAAkB,qBAAO,CAAC;AAEhC,QAAM,oBAAgB,qBAAO,KAAK;AAClC,QAAM,qBAAiB,qBAAO,CAAC;AAE/B,QAAM,wBAAoB,0BAAY,MAAM;AAC1C,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,UAAU,WAAW,UAAU,WAAW,SAAS,SAAS;AAC9D,YAAM,QAAQ,MAAM,UAAU;AAC9B,UAAI,QAAQ,GAAG;AACb,wBAAgB,WAAW;AAAA,MAC7B;AAAA,IACF;AACA,cAAU,UAAU;AAAA,EACtB,GAAG,CAAC,CAAC;AAEL,QAAM,yBAAqB,0BAAY,MAAM;AAC3C,sBAAkB;AAClB,UAAM,uBAAuB,gBAAgB;AAC7C,oBAAgB,UAAU;AAC1B,QAAI,wBAAwB,EAAG;AAC/B,iCAAW,mBAAmB,EAAE,qBAAqB,CAAC;AAAA,EACxD,GAAG,CAAC,iBAAiB,CAAC;AAEtB,QAAM,iBAAa,0BAAY,MAAM;AACnC,sBAAkB;AAClB,UAAM,uBAAuB,gBAAgB;AAC7C,oBAAgB,UAAU;AAC1B,QAAI,wBAAwB,EAAG;AAC/B,4BAAM,UAAU,EAAE,qBAAqB,GAAG,EAAE,0BAA0B,MAAM,CAAC;AAAA,EAC/E,GAAG,CAAC,iBAAiB,CAAC;AAItB,8BAAU,MAAM;AACd,kBAAc,UAAU;AAAA,EAC1B,GAAG,CAAC,QAAQ,CAAC;AAEb,8BAAU,MAAM;AACd,cAAU,UAAU,OAAO,aAAa,eAAe,SAAS,SAAS;AACzE,cAAU,UAAU,OAAO,aAAa,eAAe,SAAS,oBAAoB;AACpF,cAAU,UAAU,KAAK,IAAI;AAE7B,UAAM,UAAU,MAAM;AACpB,wBAAkB;AAClB,gBAAU,UAAU;AAAA,IACtB;AAEA,UAAM,SAAS,MAAM;AACnB,wBAAkB;AAClB,gBAAU,UAAU;AAAA,IACtB;AAEA,UAAM,aAAa,MAAM;AACvB,wBAAkB;AAClB,eAAS,UAAU;AAAA,IACrB;AAEA,UAAM,aAAa,MAAM;AACvB,wBAAkB;AAClB,eAAS,UAAU;AACnB,yBAAmB;AAAA,IACrB;AAEA,UAAM,qBAAqB,MAAM;AAC/B,wBAAkB;AAClB,UAAI,SAAS,oBAAoB,WAAW;AAC1C,kBAAU,UAAU;AAAA,MACtB,OAAO;AACL,kBAAU,UAAU;AACpB,2BAAmB;AAAA,MACrB;AAAA,IACF;AAEA,UAAM,WAAW,MAAM;AACrB,YAAM,MAAM,KAAK,IAAI;AACrB,UAAI,MAAM,eAAe,UAAU,UAAW;AAC9C,qBAAe,UAAU;AACzB,UAAI,cAAc,QAAS;AAG3B,YAAM,YAAY,OAAO,WAAW,SAAS,gBAAgB;AAC7D,YAAM,eAAe,OAAO;AAC5B,YAAM,YAAY,SAAS,gBAAgB;AAC3C,UAAI,cAAc,EAAG;AACrB,YAAM,iBAAiB,YAAY,gBAAgB;AACnD,UAAI,gBAAgB,gBAAiB;AACrC,oBAAc,UAAU;AAExB,iBAAW;AAAA,IACb;AAEA,4BAAM,iBAAiB,CAAC,GAAG,EAAE,0BAA0B,MAAM,CAAC;AAE9D,WAAO,iBAAiB,SAAS,SAAS,EAAE,SAAS,KAAK,CAAC;AAC3D,WAAO,iBAAiB,QAAQ,QAAQ,EAAE,SAAS,KAAK,CAAC;AACzD,WAAO,iBAAiB,YAAY,YAAY,EAAE,SAAS,KAAK,CAAC;AACjE,WAAO,iBAAiB,YAAY,YAAY,EAAE,SAAS,KAAK,CAAC;AACjE,WAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;AAC7D,aAAS,iBAAiB,oBAAoB,oBAAoB,EAAE,SAAS,KAAK,CAAC;AAEnF,WAAO,MAAM;AACX,aAAO,oBAAoB,SAAS,OAAO;AAC3C,aAAO,oBAAoB,QAAQ,MAAM;AACzC,aAAO,oBAAoB,YAAY,UAAU;AACjD,aAAO,oBAAoB,YAAY,UAAU;AACjD,aAAO,oBAAoB,UAAU,QAAQ;AAC7C,eAAS,oBAAoB,oBAAoB,kBAAkB;AAAA,IACrE;AAAA,EACF,GAAG,CAAC,CAAC;AACP;","names":[]}
1
+ {"version":3,"sources":["../../src/hooks/use-web-session-analytics.ts"],"sourcesContent":["/**\n * reference:\n * - Discover how long someone spends engaged on your website or app in Google Analytics](https://support.google.com/analytics/answer/11109416?hl=en)\n */\nimport { throttle } from '@shware/utils';\nimport { useCallback, useEffect, useRef } from 'react';\nimport { SESSION_TIMEOUT } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\n\n/**\n * 1. send session_start event when the page is loaded\n * 2. send scroll event when the user scrolls more than 90% of the page\n * 3. send user_engagement event when the page is hidden or the user is not focused\n */\nexport function useWebSessionAnalytics(pathname: string) {\n const isActive = useRef(true);\n const isFocused = useRef(true);\n const isVisible = useRef(true);\n\n const startTime = useRef(Date.now());\n const accumulatedTime = useRef(0);\n\n const hasSendScroll = useRef(false);\n\n const updateAccumulator = useCallback(() => {\n const now = Date.now();\n if (isFocused.current && isVisible.current && isActive.current) {\n const delta = now - startTime.current;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n accumulatedTime.current += delta;\n }\n }\n startTime.current = now;\n }, []);\n\n const sendUserEngagement = useCallback(() => {\n updateAccumulator();\n const engagement_time_msec = accumulatedTime.current;\n accumulatedTime.current = 0;\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec });\n }, [updateAccumulator]);\n\n const sendScroll = useCallback(() => {\n updateAccumulator();\n const engagement_time_msec = accumulatedTime.current;\n accumulatedTime.current = 0;\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec }, { enableThirdPartyTracking: false });\n }, [updateAccumulator]);\n\n // reset scroll state when the pathname changes, so we can send scroll when the user navigates to\n // a new page\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n isFocused.current = typeof document !== 'undefined' && document.hasFocus();\n isVisible.current = typeof document !== 'undefined' && document.visibilityState === 'visible';\n startTime.current = Date.now();\n\n track('session_start', {}, { enableThirdPartyTracking: false });\n\n const onFocus = () => {\n updateAccumulator();\n isFocused.current = true;\n };\n\n const onBlur = () => {\n updateAccumulator();\n isFocused.current = false;\n };\n\n const onPageShow = () => {\n updateAccumulator();\n isActive.current = true;\n };\n\n const onPageHide = () => {\n updateAccumulator();\n isActive.current = false;\n sendUserEngagement();\n };\n\n const onVisibilityChange = () => {\n updateAccumulator();\n if (document.visibilityState === 'visible') {\n isVisible.current = true;\n } else {\n isVisible.current = false;\n sendUserEngagement();\n }\n };\n\n const onScroll = throttle(() => {\n updateAccumulator();\n if (hasSendScroll.current) return;\n\n // only send scroll when the user has scrolled more than 90% of the page\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return;\n const scrollPercent = (scrollTop + windowHeight) / docHeight;\n if (scrollPercent < 0.9) return;\n hasSendScroll.current = true;\n\n sendScroll();\n }, 500);\n\n const checkpointEvents = ['mousedown', 'keydown', 'touchstart'];\n const checkpoint = throttle(updateAccumulator, 1000);\n\n window.addEventListener('focus', onFocus, { passive: true });\n window.addEventListener('blur', onBlur, { passive: true });\n window.addEventListener('pageshow', onPageShow, { passive: true });\n window.addEventListener('pagehide', onPageHide, { passive: true });\n window.addEventListener('scroll', onScroll, { passive: true });\n document.addEventListener('visibilitychange', onVisibilityChange, { passive: true });\n\n // save checkpoint\n checkpointEvents.forEach((event) => {\n window.addEventListener(event, checkpoint, { passive: true, capture: true });\n });\n\n return () => {\n window.removeEventListener('focus', onFocus);\n window.removeEventListener('blur', onBlur);\n window.removeEventListener('pageshow', onPageShow);\n window.removeEventListener('pagehide', onPageHide);\n window.removeEventListener('scroll', onScroll);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n\n checkpointEvents.forEach((event) => {\n window.removeEventListener(event, checkpoint);\n });\n\n onScroll.cancel();\n checkpoint.cancel();\n };\n }, []);\n}\n"],"mappings":"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","names":[]}
@@ -1,8 +1,8 @@
1
1
  // src/hooks/use-web-session-analytics.ts
2
+ import { throttle } from "@shware/utils";
2
3
  import { useCallback, useEffect, useRef } from "react";
4
+ import { SESSION_TIMEOUT } from "../setup/session.mjs";
3
5
  import { sendBeacon, track } from "../track/index.mjs";
4
- var scrollGap = 500;
5
- var scrollThreshold = 0.9;
6
6
  function useWebSessionAnalytics(pathname) {
7
7
  const isActive = useRef(true);
8
8
  const isFocused = useRef(true);
@@ -10,12 +10,11 @@ function useWebSessionAnalytics(pathname) {
10
10
  const startTime = useRef(Date.now());
11
11
  const accumulatedTime = useRef(0);
12
12
  const hasSendScroll = useRef(false);
13
- const lastScrollTime = useRef(0);
14
13
  const updateAccumulator = useCallback(() => {
15
14
  const now = Date.now();
16
15
  if (isFocused.current && isVisible.current && isActive.current) {
17
16
  const delta = now - startTime.current;
18
- if (delta > 0) {
17
+ if (delta > 0 && delta < SESSION_TIMEOUT) {
19
18
  accumulatedTime.current += delta;
20
19
  }
21
20
  }
@@ -42,6 +41,7 @@ function useWebSessionAnalytics(pathname) {
42
41
  isFocused.current = typeof document !== "undefined" && document.hasFocus();
43
42
  isVisible.current = typeof document !== "undefined" && document.visibilityState === "visible";
44
43
  startTime.current = Date.now();
44
+ track("session_start", {}, { enableThirdPartyTracking: false });
45
45
  const onFocus = () => {
46
46
  updateAccumulator();
47
47
  isFocused.current = true;
@@ -68,27 +68,29 @@ function useWebSessionAnalytics(pathname) {
68
68
  sendUserEngagement();
69
69
  }
70
70
  };
71
- const onScroll = () => {
72
- const now = Date.now();
73
- if (now - lastScrollTime.current < scrollGap) return;
74
- lastScrollTime.current = now;
71
+ const onScroll = throttle(() => {
72
+ updateAccumulator();
75
73
  if (hasSendScroll.current) return;
76
74
  const scrollTop = window.scrollY || document.documentElement.scrollTop;
77
75
  const windowHeight = window.innerHeight;
78
76
  const docHeight = document.documentElement.scrollHeight;
79
77
  if (docHeight === 0) return;
80
78
  const scrollPercent = (scrollTop + windowHeight) / docHeight;
81
- if (scrollPercent < scrollThreshold) return;
79
+ if (scrollPercent < 0.9) return;
82
80
  hasSendScroll.current = true;
83
81
  sendScroll();
84
- };
85
- track("session_start", {}, { enableThirdPartyTracking: false });
82
+ }, 500);
83
+ const checkpointEvents = ["mousedown", "keydown", "touchstart"];
84
+ const checkpoint = throttle(updateAccumulator, 1e3);
86
85
  window.addEventListener("focus", onFocus, { passive: true });
87
86
  window.addEventListener("blur", onBlur, { passive: true });
88
87
  window.addEventListener("pageshow", onPageShow, { passive: true });
89
88
  window.addEventListener("pagehide", onPageHide, { passive: true });
90
89
  window.addEventListener("scroll", onScroll, { passive: true });
91
90
  document.addEventListener("visibilitychange", onVisibilityChange, { passive: true });
91
+ checkpointEvents.forEach((event) => {
92
+ window.addEventListener(event, checkpoint, { passive: true, capture: true });
93
+ });
92
94
  return () => {
93
95
  window.removeEventListener("focus", onFocus);
94
96
  window.removeEventListener("blur", onBlur);
@@ -96,6 +98,11 @@ function useWebSessionAnalytics(pathname) {
96
98
  window.removeEventListener("pagehide", onPageHide);
97
99
  window.removeEventListener("scroll", onScroll);
98
100
  document.removeEventListener("visibilitychange", onVisibilityChange);
101
+ checkpointEvents.forEach((event) => {
102
+ window.removeEventListener(event, checkpoint);
103
+ });
104
+ onScroll.cancel();
105
+ checkpoint.cancel();
99
106
  };
100
107
  }, []);
101
108
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/use-web-session-analytics.ts"],"sourcesContent":["/**\n * reference:\n * - Discover how long someone spends engaged on your website or app in Google Analytics](https://support.google.com/analytics/answer/11109416?hl=en)\n */\nimport { useCallback, useEffect, useRef } from 'react';\nimport { sendBeacon, track } from '../track/index';\n\nconst scrollGap = 500;\nconst scrollThreshold = 0.9;\n\n/**\n * 1. send session_start event when the page is loaded\n * 2. send scroll event when the user scrolls more than 90% of the page\n * 3. send user_engagement event when the page is hidden or the user is not focused\n */\nexport function useWebSessionAnalytics(pathname: string) {\n const isActive = useRef(true);\n const isFocused = useRef(true);\n const isVisible = useRef(true);\n\n const startTime = useRef(Date.now());\n const accumulatedTime = useRef(0);\n\n const hasSendScroll = useRef(false);\n const lastScrollTime = useRef(0);\n\n const updateAccumulator = useCallback(() => {\n const now = Date.now();\n if (isFocused.current && isVisible.current && isActive.current) {\n const delta = now - startTime.current;\n if (delta > 0) {\n accumulatedTime.current += delta;\n }\n }\n startTime.current = now;\n }, []);\n\n const sendUserEngagement = useCallback(() => {\n updateAccumulator();\n const engagement_time_msec = accumulatedTime.current;\n accumulatedTime.current = 0;\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec });\n }, [updateAccumulator]);\n\n const sendScroll = useCallback(() => {\n updateAccumulator();\n const engagement_time_msec = accumulatedTime.current;\n accumulatedTime.current = 0;\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec }, { enableThirdPartyTracking: false });\n }, [updateAccumulator]);\n\n // reset scroll state when the pathname changes, so we can send scroll when the user navigates to\n // a new page\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n isFocused.current = typeof document !== 'undefined' && document.hasFocus();\n isVisible.current = typeof document !== 'undefined' && document.visibilityState === 'visible';\n startTime.current = Date.now();\n\n const onFocus = () => {\n updateAccumulator();\n isFocused.current = true;\n };\n\n const onBlur = () => {\n updateAccumulator();\n isFocused.current = false;\n };\n\n const onPageShow = () => {\n updateAccumulator();\n isActive.current = true;\n };\n\n const onPageHide = () => {\n updateAccumulator();\n isActive.current = false;\n sendUserEngagement();\n };\n\n const onVisibilityChange = () => {\n updateAccumulator();\n if (document.visibilityState === 'visible') {\n isVisible.current = true;\n } else {\n isVisible.current = false;\n sendUserEngagement();\n }\n };\n\n const onScroll = () => {\n const now = Date.now();\n if (now - lastScrollTime.current < scrollGap) return;\n lastScrollTime.current = now;\n if (hasSendScroll.current) return;\n\n // only send scroll when the user has scrolled more than 90% of the page\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return;\n const scrollPercent = (scrollTop + windowHeight) / docHeight;\n if (scrollPercent < scrollThreshold) return;\n hasSendScroll.current = true;\n\n sendScroll();\n };\n\n track('session_start', {}, { enableThirdPartyTracking: false });\n\n window.addEventListener('focus', onFocus, { passive: true });\n window.addEventListener('blur', onBlur, { passive: true });\n window.addEventListener('pageshow', onPageShow, { passive: true });\n window.addEventListener('pagehide', onPageHide, { passive: true });\n window.addEventListener('scroll', onScroll, { passive: true });\n document.addEventListener('visibilitychange', onVisibilityChange, { passive: true });\n\n return () => {\n window.removeEventListener('focus', onFocus);\n window.removeEventListener('blur', onBlur);\n window.removeEventListener('pageshow', onPageShow);\n window.removeEventListener('pagehide', onPageHide);\n window.removeEventListener('scroll', onScroll);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n };\n }, []);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/hooks/use-web-session-analytics.ts"],"sourcesContent":["/**\n * reference:\n * - Discover how long someone spends engaged on your website or app in Google Analytics](https://support.google.com/analytics/answer/11109416?hl=en)\n */\nimport { throttle } from '@shware/utils';\nimport { useCallback, useEffect, useRef } from 'react';\nimport { SESSION_TIMEOUT } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\n\n/**\n * 1. send session_start event when the page is loaded\n * 2. send scroll event when the user scrolls more than 90% of the page\n * 3. send user_engagement event when the page is hidden or the user is not focused\n */\nexport function useWebSessionAnalytics(pathname: string) {\n const isActive = useRef(true);\n const isFocused = useRef(true);\n const isVisible = useRef(true);\n\n const startTime = useRef(Date.now());\n const accumulatedTime = useRef(0);\n\n const hasSendScroll = useRef(false);\n\n const updateAccumulator = useCallback(() => {\n const now = Date.now();\n if (isFocused.current && isVisible.current && isActive.current) {\n const delta = now - startTime.current;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n accumulatedTime.current += delta;\n }\n }\n startTime.current = now;\n }, []);\n\n const sendUserEngagement = useCallback(() => {\n updateAccumulator();\n const engagement_time_msec = accumulatedTime.current;\n accumulatedTime.current = 0;\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec });\n }, [updateAccumulator]);\n\n const sendScroll = useCallback(() => {\n updateAccumulator();\n const engagement_time_msec = accumulatedTime.current;\n accumulatedTime.current = 0;\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec }, { enableThirdPartyTracking: false });\n }, [updateAccumulator]);\n\n // reset scroll state when the pathname changes, so we can send scroll when the user navigates to\n // a new page\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n isFocused.current = typeof document !== 'undefined' && document.hasFocus();\n isVisible.current = typeof document !== 'undefined' && document.visibilityState === 'visible';\n startTime.current = Date.now();\n\n track('session_start', {}, { enableThirdPartyTracking: false });\n\n const onFocus = () => {\n updateAccumulator();\n isFocused.current = true;\n };\n\n const onBlur = () => {\n updateAccumulator();\n isFocused.current = false;\n };\n\n const onPageShow = () => {\n updateAccumulator();\n isActive.current = true;\n };\n\n const onPageHide = () => {\n updateAccumulator();\n isActive.current = false;\n sendUserEngagement();\n };\n\n const onVisibilityChange = () => {\n updateAccumulator();\n if (document.visibilityState === 'visible') {\n isVisible.current = true;\n } else {\n isVisible.current = false;\n sendUserEngagement();\n }\n };\n\n const onScroll = throttle(() => {\n updateAccumulator();\n if (hasSendScroll.current) return;\n\n // only send scroll when the user has scrolled more than 90% of the page\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return;\n const scrollPercent = (scrollTop + windowHeight) / docHeight;\n if (scrollPercent < 0.9) return;\n hasSendScroll.current = true;\n\n sendScroll();\n }, 500);\n\n const checkpointEvents = ['mousedown', 'keydown', 'touchstart'];\n const checkpoint = throttle(updateAccumulator, 1000);\n\n window.addEventListener('focus', onFocus, { passive: true });\n window.addEventListener('blur', onBlur, { passive: true });\n window.addEventListener('pageshow', onPageShow, { passive: true });\n window.addEventListener('pagehide', onPageHide, { passive: true });\n window.addEventListener('scroll', onScroll, { passive: true });\n document.addEventListener('visibilitychange', onVisibilityChange, { passive: true });\n\n // save checkpoint\n checkpointEvents.forEach((event) => {\n window.addEventListener(event, checkpoint, { passive: true, capture: true });\n });\n\n return () => {\n window.removeEventListener('focus', onFocus);\n window.removeEventListener('blur', onBlur);\n window.removeEventListener('pageshow', onPageShow);\n window.removeEventListener('pagehide', onPageHide);\n window.removeEventListener('scroll', onScroll);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n\n checkpointEvents.forEach((event) => {\n window.removeEventListener(event, checkpoint);\n });\n\n onScroll.cancel();\n checkpoint.cancel();\n };\n }, []);\n}\n"],"mappings":"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","names":[]}
@@ -24,10 +24,10 @@ __export(link_exports, {
24
24
  getLink: () => getLink
25
25
  });
26
26
  module.exports = __toCommonJS(link_exports);
27
+ var import_utils = require("@shware/utils");
27
28
  var import_setup = require("../setup/index.cjs");
28
- var import_fetch = require("../utils/fetch.cjs");
29
29
  async function createLink(dto) {
30
- const response = await (0, import_fetch.fetch)(`${import_setup.config.endpoint}/links`, {
30
+ const response = await (0, import_utils.fetch)(`${import_setup.config.endpoint}/links`, {
31
31
  method: "POST",
32
32
  credentials: "include",
33
33
  headers: await import_setup.config.getHeaders(),
@@ -40,7 +40,7 @@ async function createLink(dto) {
40
40
  }
41
41
  async function getLink(id) {
42
42
  try {
43
- const response = await (0, import_fetch.fetch)(`${import_setup.config.endpoint}/links/${id}`, {
43
+ const response = await (0, import_utils.fetch)(`${import_setup.config.endpoint}/links/${id}`, {
44
44
  method: "GET",
45
45
  credentials: "include",
46
46
  headers: await import_setup.config.getHeaders()
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/link/index.ts"],"sourcesContent":["import { config } from '../setup/index';\nimport { fetch } from '../utils/fetch';\nimport type { CreateLinkDTO } from '../schema/index';\n\nexport interface Link extends CreateLinkDTO {\n id: string;\n created_at: string;\n}\n\nexport async function createLink(dto: CreateLinkDTO) {\n const response = await fetch(`${config.endpoint}/links`, {\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 link: ${response.status} ${await response.text()}`);\n }\n return response.json() as Promise<Link>;\n}\n\nexport async function getLink(id: string): Promise<Link | null> {\n try {\n const response = await fetch(`${config.endpoint}/links/${id}`, {\n method: 'GET',\n credentials: 'include',\n headers: await config.getHeaders(),\n });\n\n if (!response.ok) {\n console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);\n return null;\n }\n return response.json() as Promise<Link>;\n } catch {\n console.error(`Failed to get link(${id}): network error`);\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAuB;AACvB,mBAAsB;AAQtB,eAAsB,WAAW,KAAoB;AACnD,QAAM,WAAW,UAAM,oBAAM,GAAG,oBAAO,QAAQ,UAAU;AAAA,IACvD,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,MAAM,oBAAO,WAAW;AAAA,IACjC,MAAM,KAAK,UAAU,GAAG;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AAAA,EACtF;AACA,SAAO,SAAS,KAAK;AACvB;AAEA,eAAsB,QAAQ,IAAkC;AAC9D,MAAI;AACF,UAAM,WAAW,UAAM,oBAAM,GAAG,oBAAO,QAAQ,UAAU,EAAE,IAAI;AAAA,MAC7D,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,MAAM,oBAAO,WAAW;AAAA,IACnC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,sBAAsB,EAAE,MAAM,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AACtF,aAAO;AAAA,IACT;AACA,WAAO,SAAS,KAAK;AAAA,EACvB,QAAQ;AACN,YAAQ,MAAM,sBAAsB,EAAE,kBAAkB;AACxD,WAAO;AAAA,EACT;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/link/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { config } from '../setup/index';\nimport type { CreateLinkDTO } from '../schema/index';\n\nexport interface Link extends CreateLinkDTO {\n id: string;\n created_at: string;\n}\n\nexport async function createLink(dto: CreateLinkDTO) {\n const response = await fetch(`${config.endpoint}/links`, {\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 link: ${response.status} ${await response.text()}`);\n }\n return response.json() as Promise<Link>;\n}\n\nexport async function getLink(id: string): Promise<Link | null> {\n try {\n const response = await fetch(`${config.endpoint}/links/${id}`, {\n method: 'GET',\n credentials: 'include',\n headers: await config.getHeaders(),\n });\n\n if (!response.ok) {\n console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);\n return null;\n }\n return response.json() as Promise<Link>;\n } catch {\n console.error(`Failed to get link(${id}): network error`);\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAsB;AACtB,mBAAuB;AAQvB,eAAsB,WAAW,KAAoB;AACnD,QAAM,WAAW,UAAM,oBAAM,GAAG,oBAAO,QAAQ,UAAU;AAAA,IACvD,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,MAAM,oBAAO,WAAW;AAAA,IACjC,MAAM,KAAK,UAAU,GAAG;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AAAA,EACtF;AACA,SAAO,SAAS,KAAK;AACvB;AAEA,eAAsB,QAAQ,IAAkC;AAC9D,MAAI;AACF,UAAM,WAAW,UAAM,oBAAM,GAAG,oBAAO,QAAQ,UAAU,EAAE,IAAI;AAAA,MAC7D,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,MAAM,oBAAO,WAAW;AAAA,IACnC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,sBAAsB,EAAE,MAAM,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AACtF,aAAO;AAAA,IACT;AACA,WAAO,SAAS,KAAK;AAAA,EACvB,QAAQ;AACN,YAAQ,MAAM,sBAAsB,EAAE,kBAAkB;AACxD,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  // src/link/index.ts
2
+ import { fetch } from "@shware/utils";
2
3
  import { config } from "../setup/index.mjs";
3
- import { fetch } from "../utils/fetch.mjs";
4
4
  async function createLink(dto) {
5
5
  const response = await fetch(`${config.endpoint}/links`, {
6
6
  method: "POST",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/link/index.ts"],"sourcesContent":["import { config } from '../setup/index';\nimport { fetch } from '../utils/fetch';\nimport type { CreateLinkDTO } from '../schema/index';\n\nexport interface Link extends CreateLinkDTO {\n id: string;\n created_at: string;\n}\n\nexport async function createLink(dto: CreateLinkDTO) {\n const response = await fetch(`${config.endpoint}/links`, {\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 link: ${response.status} ${await response.text()}`);\n }\n return response.json() as Promise<Link>;\n}\n\nexport async function getLink(id: string): Promise<Link | null> {\n try {\n const response = await fetch(`${config.endpoint}/links/${id}`, {\n method: 'GET',\n credentials: 'include',\n headers: await config.getHeaders(),\n });\n\n if (!response.ok) {\n console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);\n return null;\n }\n return response.json() as Promise<Link>;\n } catch {\n console.error(`Failed to get link(${id}): network error`);\n return null;\n }\n}\n"],"mappings":";AAAA,SAAS,cAAc;AACvB,SAAS,aAAa;AAQtB,eAAsB,WAAW,KAAoB;AACnD,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,QAAQ,UAAU;AAAA,IACvD,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,MAAM,OAAO,WAAW;AAAA,IACjC,MAAM,KAAK,UAAU,GAAG;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AAAA,EACtF;AACA,SAAO,SAAS,KAAK;AACvB;AAEA,eAAsB,QAAQ,IAAkC;AAC9D,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,QAAQ,UAAU,EAAE,IAAI;AAAA,MAC7D,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,MAAM,OAAO,WAAW;AAAA,IACnC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,sBAAsB,EAAE,MAAM,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AACtF,aAAO;AAAA,IACT;AACA,WAAO,SAAS,KAAK;AAAA,EACvB,QAAQ;AACN,YAAQ,MAAM,sBAAsB,EAAE,kBAAkB;AACxD,WAAO;AAAA,EACT;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/link/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { config } from '../setup/index';\nimport type { CreateLinkDTO } from '../schema/index';\n\nexport interface Link extends CreateLinkDTO {\n id: string;\n created_at: string;\n}\n\nexport async function createLink(dto: CreateLinkDTO) {\n const response = await fetch(`${config.endpoint}/links`, {\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 link: ${response.status} ${await response.text()}`);\n }\n return response.json() as Promise<Link>;\n}\n\nexport async function getLink(id: string): Promise<Link | null> {\n try {\n const response = await fetch(`${config.endpoint}/links/${id}`, {\n method: 'GET',\n credentials: 'include',\n headers: await config.getHeaders(),\n });\n\n if (!response.ok) {\n console.error(`Failed to get link(${id}): ${response.status} ${await response.text()}`);\n return null;\n }\n return response.json() as Promise<Link>;\n } catch {\n console.error(`Failed to get link(${id}): network error`);\n return null;\n }\n}\n"],"mappings":";AAAA,SAAS,aAAa;AACtB,SAAS,cAAc;AAQvB,eAAsB,WAAW,KAAoB;AACnD,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,QAAQ,UAAU;AAAA,IACvD,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,MAAM,OAAO,WAAW;AAAA,IACjC,MAAM,KAAK,UAAU,GAAG;AAAA,EAC1B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AAAA,EACtF;AACA,SAAO,SAAS,KAAK;AACvB;AAEA,eAAsB,QAAQ,IAAkC;AAC9D,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,QAAQ,UAAU,EAAE,IAAI;AAAA,MAC7D,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,MAAM,OAAO,WAAW;AAAA,IACnC,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,sBAAsB,EAAE,MAAM,SAAS,MAAM,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE;AACtF,aAAO;AAAA,IACT;AACA,WAAO,SAAS,KAAK;AAAA,EACvB,QAAQ;AACN,YAAQ,MAAM,sBAAsB,EAAE,kBAAkB;AACxD,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -24,7 +24,7 @@ __export(linkedin_conversions_api_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(linkedin_conversions_api_exports);
26
26
  var import_crypto = require("crypto");
27
- var import_fetch = require("../utils/fetch.cjs");
27
+ var import_utils = require("@shware/utils");
28
28
  var import_field = require("../utils/field.cjs");
29
29
  var import_ignore_events = require("./ignore-events.cjs");
30
30
  async function sendEvents(accessToken, config, events, data = {}) {
@@ -72,7 +72,7 @@ async function sendEvents(accessToken, config, events, data = {}) {
72
72
  };
73
73
  if (dto.elements.length === 0) return;
74
74
  try {
75
- const response = await (0, import_fetch.fetch)("https://api.linkedin.com/rest/conversionEvents", {
75
+ const response = await (0, import_utils.fetch)("https://api.linkedin.com/rest/conversionEvents", {
76
76
  method: "POST",
77
77
  headers: {
78
78
  Authorization: `Bearer ${accessToken}`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/linkedin-conversions-api.ts"],"sourcesContent":["/**\n * Conversions API Payload Builder: https://www.linkedin.com/developers/payload-builder\n * https://learn.microsoft.com/en-us/linkedin/marketing/conversions/conversions-overview?view=li-lms-2025-09\n */\nimport { createHash } from 'crypto';\nimport { fetch } from '../utils/fetch';\nimport { getFirst } from '../utils/field';\nimport { IGNORE_EVENTS } from './ignore-events';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\n\ntype UserIdType =\n | 'SHA256_EMAIL'\n | 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID'\n | 'ACXIOM_ID'\n | 'ORACLE_MOAT_ID';\n\nexport interface CreateLinkedinEventDTO {\n /**\n * For any conversion that you want to send through multiple methods, such as Insight Tag and\n * Conversions API, you must create a conversion rule for each data source (browser and server).\n * Then, you can implement a logic to pick up the eventId from the browser and send it with the\n * corresponding event from your server. If we receive an Insight Tag event and a Conversions API\n * event from the same account with the same eventId, we discard the Conversions API event and\n * count only the Insight Tag event in campaign reporting.\n */\n eventId?: string;\n\n /**\n * Replace <id> with the conversion ID extracted when creating the conversion rule\n * (e.g. urn:lla:llaPartnerConversion:<id>).\n */\n conversion: `urn:lla:llaPartnerConversion:${number}`;\n\n /** Epoch timestamp in milliseconds at which the conversion event happened. */\n conversionHappenedAt: number;\n conversionValue: { currencyCode: string; amount: string };\n user: {\n userIds: { idType: UserIdType; idValue: string }[];\n userInfo?: {\n firstName?: string;\n lastName?: string;\n companyName?: string;\n countryCode?: string;\n title?: string;\n };\n\n /**\n * The maximum supported size of the list is 1 at the moment. If the list contains multiple\n * values, only the first value will be used.\n */\n externalIds?: [string, ...string[]];\n\n /**\n * This is generated when users submit the Linkedin Lead-gen form\n * (e.g. urn:li:leadGenFormResponse:<id>).\n */\n lead?: `urn:li:leadGenFormResponse:${string}`;\n };\n}\n\nexport interface CreateMultipleLinkedinEventsDTO {\n elements: CreateLinkedinEventDTO[];\n}\n\nexport type LinkedinConversionConfig = Record<Lowercase<string>, number>;\n\nexport async function sendEvents(\n accessToken: string,\n config: LinkedinConversionConfig,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {}\n) {\n const eventNames = Object.keys(config);\n const address = getFirst(data.address);\n const userIds: { idType: UserIdType; idValue: string }[] = [];\n const externalIds: [string, ...string[]] | undefined = data.user_id ? [data.user_id] : undefined;\n const userInfo =\n address && address.first_name && address.last_name\n ? {\n firstName: address.first_name,\n lastName: address.last_name,\n countryCode: address.country,\n }\n : undefined;\n\n if (data.email) {\n const email = getFirst(data.email);\n if (email)\n userIds.push({\n idType: 'SHA256_EMAIL',\n idValue: createHash('sha256').update(email).digest('hex'),\n });\n }\n\n const dto: CreateMultipleLinkedinEventsDTO = {\n elements: events\n .filter((event) => eventNames.includes(event.name) && !IGNORE_EVENTS.includes(event.name))\n .map((event) => ({\n eventId: event.id,\n conversion: `urn:lla:llaPartnerConversion:${config[event.name]}`,\n conversionHappenedAt: Date.now(),\n conversionValue: {\n currencyCode: event.properties?.currency?.toUpperCase() ?? 'USD',\n amount: event.properties?.value?.toString() ?? '0',\n },\n user: {\n userIds: event.tags.li_fat_id\n ? [\n {\n idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID',\n idValue: event.tags.li_fat_id,\n },\n ...userIds,\n ]\n : userIds,\n userInfo,\n externalIds,\n },\n })),\n };\n\n if (dto.elements.length === 0) return;\n try {\n const response = await fetch('https://api.linkedin.com/rest/conversionEvents', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'LinkedIn-Version': '202509',\n 'X-Restli-Protocol-Version': '2.0.0',\n 'X-RestLi-Method': 'BATCH_CREATE',\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 LinkedIn conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send LinkedIn conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,oBAA2B;AAC3B,mBAAsB;AACtB,mBAAyB;AACzB,2BAA8B;AA2D9B,eAAsB,WACpB,aACA,QAEA,QACA,OAAyB,CAAC,GAC1B;AACA,QAAM,aAAa,OAAO,KAAK,MAAM;AACrC,QAAM,cAAU,uBAAS,KAAK,OAAO;AACrC,QAAM,UAAqD,CAAC;AAC5D,QAAM,cAAiD,KAAK,UAAU,CAAC,KAAK,OAAO,IAAI;AACvF,QAAM,WACJ,WAAW,QAAQ,cAAc,QAAQ,YACrC;AAAA,IACE,WAAW,QAAQ;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,aAAa,QAAQ;AAAA,EACvB,IACA;AAEN,MAAI,KAAK,OAAO;AACd,UAAM,YAAQ,uBAAS,KAAK,KAAK;AACjC,QAAI;AACF,cAAQ,KAAK;AAAA,QACX,QAAQ;AAAA,QACR,aAAS,0BAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAAA,MAC1D,CAAC;AAAA,EACL;AAEA,QAAM,MAAuC;AAAA,IAC3C,UAAU,OACP,OAAO,CAAC,UAAU,WAAW,SAAS,MAAM,IAAI,KAAK,CAAC,mCAAc,SAAS,MAAM,IAAI,CAAC,EACxF,IAAI,CAAC,UAAO;AAlGnB;AAkGuB;AAAA,QACf,SAAS,MAAM;AAAA,QACf,YAAY,gCAAgC,OAAO,MAAM,IAAI,CAAC;AAAA,QAC9D,sBAAsB,KAAK,IAAI;AAAA,QAC/B,iBAAiB;AAAA,UACf,gBAAc,iBAAM,eAAN,mBAAkB,aAAlB,mBAA4B,kBAAiB;AAAA,UAC3D,UAAQ,iBAAM,eAAN,mBAAkB,UAAlB,mBAAyB,eAAc;AAAA,QACjD;AAAA,QACA,MAAM;AAAA,UACJ,SAAS,MAAM,KAAK,YAChB;AAAA,YACE;AAAA,cACE,QAAQ;AAAA,cACR,SAAS,MAAM,KAAK;AAAA,YACtB;AAAA,YACA,GAAG;AAAA,UACL,IACA;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,KAAE;AAAA,EACN;AAEA,MAAI,IAAI,SAAS,WAAW,EAAG;AAC/B,MAAI;AACF,UAAM,WAAW,UAAM,oBAAM,kDAAkD;AAAA,MAC7E,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,WAAW;AAAA,QACpC,gBAAgB;AAAA,QAChB,oBAAoB;AAAA,QACpB,6BAA6B;AAAA,QAC7B,mBAAmB;AAAA,MACrB;AAAA,MACA,MAAM,KAAK,UAAU,GAAG;AAAA,IAC1B,CAAC;AAED,QAAI,SAAS,GAAI;AACjB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,YAAQ,MAAM,+CAA+C,MAAM,WAAW,OAAO,EAAE;AAAA,EACzF,SAAS,OAAO;AACd,YAAQ,MAAM,sDAAsD,KAAK;AAAA,EAC3E;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/server/linkedin-conversions-api.ts"],"sourcesContent":["/**\n * Conversions API Payload Builder: https://www.linkedin.com/developers/payload-builder\n * https://learn.microsoft.com/en-us/linkedin/marketing/conversions/conversions-overview?view=li-lms-2025-09\n */\nimport { createHash } from 'crypto';\nimport { fetch } from '@shware/utils';\nimport { getFirst } from '../utils/field';\nimport { IGNORE_EVENTS } from './ignore-events';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\n\ntype UserIdType =\n | 'SHA256_EMAIL'\n | 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID'\n | 'ACXIOM_ID'\n | 'ORACLE_MOAT_ID';\n\nexport interface CreateLinkedinEventDTO {\n /**\n * For any conversion that you want to send through multiple methods, such as Insight Tag and\n * Conversions API, you must create a conversion rule for each data source (browser and server).\n * Then, you can implement a logic to pick up the eventId from the browser and send it with the\n * corresponding event from your server. If we receive an Insight Tag event and a Conversions API\n * event from the same account with the same eventId, we discard the Conversions API event and\n * count only the Insight Tag event in campaign reporting.\n */\n eventId?: string;\n\n /**\n * Replace <id> with the conversion ID extracted when creating the conversion rule\n * (e.g. urn:lla:llaPartnerConversion:<id>).\n */\n conversion: `urn:lla:llaPartnerConversion:${number}`;\n\n /** Epoch timestamp in milliseconds at which the conversion event happened. */\n conversionHappenedAt: number;\n conversionValue: { currencyCode: string; amount: string };\n user: {\n userIds: { idType: UserIdType; idValue: string }[];\n userInfo?: {\n firstName?: string;\n lastName?: string;\n companyName?: string;\n countryCode?: string;\n title?: string;\n };\n\n /**\n * The maximum supported size of the list is 1 at the moment. If the list contains multiple\n * values, only the first value will be used.\n */\n externalIds?: [string, ...string[]];\n\n /**\n * This is generated when users submit the Linkedin Lead-gen form\n * (e.g. urn:li:leadGenFormResponse:<id>).\n */\n lead?: `urn:li:leadGenFormResponse:${string}`;\n };\n}\n\nexport interface CreateMultipleLinkedinEventsDTO {\n elements: CreateLinkedinEventDTO[];\n}\n\nexport type LinkedinConversionConfig = Record<Lowercase<string>, number>;\n\nexport async function sendEvents(\n accessToken: string,\n config: LinkedinConversionConfig,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {}\n) {\n const eventNames = Object.keys(config);\n const address = getFirst(data.address);\n const userIds: { idType: UserIdType; idValue: string }[] = [];\n const externalIds: [string, ...string[]] | undefined = data.user_id ? [data.user_id] : undefined;\n const userInfo =\n address && address.first_name && address.last_name\n ? {\n firstName: address.first_name,\n lastName: address.last_name,\n countryCode: address.country,\n }\n : undefined;\n\n if (data.email) {\n const email = getFirst(data.email);\n if (email)\n userIds.push({\n idType: 'SHA256_EMAIL',\n idValue: createHash('sha256').update(email).digest('hex'),\n });\n }\n\n const dto: CreateMultipleLinkedinEventsDTO = {\n elements: events\n .filter((event) => eventNames.includes(event.name) && !IGNORE_EVENTS.includes(event.name))\n .map((event) => ({\n eventId: event.id,\n conversion: `urn:lla:llaPartnerConversion:${config[event.name]}`,\n conversionHappenedAt: Date.now(),\n conversionValue: {\n currencyCode: event.properties?.currency?.toUpperCase() ?? 'USD',\n amount: event.properties?.value?.toString() ?? '0',\n },\n user: {\n userIds: event.tags.li_fat_id\n ? [\n {\n idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID',\n idValue: event.tags.li_fat_id,\n },\n ...userIds,\n ]\n : userIds,\n userInfo,\n externalIds,\n },\n })),\n };\n\n if (dto.elements.length === 0) return;\n try {\n const response = await fetch('https://api.linkedin.com/rest/conversionEvents', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'LinkedIn-Version': '202509',\n 'X-Restli-Protocol-Version': '2.0.0',\n 'X-RestLi-Method': 'BATCH_CREATE',\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 LinkedIn conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send LinkedIn conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,oBAA2B;AAC3B,mBAAsB;AACtB,mBAAyB;AACzB,2BAA8B;AA2D9B,eAAsB,WACpB,aACA,QAEA,QACA,OAAyB,CAAC,GAC1B;AACA,QAAM,aAAa,OAAO,KAAK,MAAM;AACrC,QAAM,cAAU,uBAAS,KAAK,OAAO;AACrC,QAAM,UAAqD,CAAC;AAC5D,QAAM,cAAiD,KAAK,UAAU,CAAC,KAAK,OAAO,IAAI;AACvF,QAAM,WACJ,WAAW,QAAQ,cAAc,QAAQ,YACrC;AAAA,IACE,WAAW,QAAQ;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,aAAa,QAAQ;AAAA,EACvB,IACA;AAEN,MAAI,KAAK,OAAO;AACd,UAAM,YAAQ,uBAAS,KAAK,KAAK;AACjC,QAAI;AACF,cAAQ,KAAK;AAAA,QACX,QAAQ;AAAA,QACR,aAAS,0BAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAAA,MAC1D,CAAC;AAAA,EACL;AAEA,QAAM,MAAuC;AAAA,IAC3C,UAAU,OACP,OAAO,CAAC,UAAU,WAAW,SAAS,MAAM,IAAI,KAAK,CAAC,mCAAc,SAAS,MAAM,IAAI,CAAC,EACxF,IAAI,CAAC,UAAO;AAlGnB;AAkGuB;AAAA,QACf,SAAS,MAAM;AAAA,QACf,YAAY,gCAAgC,OAAO,MAAM,IAAI,CAAC;AAAA,QAC9D,sBAAsB,KAAK,IAAI;AAAA,QAC/B,iBAAiB;AAAA,UACf,gBAAc,iBAAM,eAAN,mBAAkB,aAAlB,mBAA4B,kBAAiB;AAAA,UAC3D,UAAQ,iBAAM,eAAN,mBAAkB,UAAlB,mBAAyB,eAAc;AAAA,QACjD;AAAA,QACA,MAAM;AAAA,UACJ,SAAS,MAAM,KAAK,YAChB;AAAA,YACE;AAAA,cACE,QAAQ;AAAA,cACR,SAAS,MAAM,KAAK;AAAA,YACtB;AAAA,YACA,GAAG;AAAA,UACL,IACA;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,KAAE;AAAA,EACN;AAEA,MAAI,IAAI,SAAS,WAAW,EAAG;AAC/B,MAAI;AACF,UAAM,WAAW,UAAM,oBAAM,kDAAkD;AAAA,MAC7E,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,WAAW;AAAA,QACpC,gBAAgB;AAAA,QAChB,oBAAoB;AAAA,QACpB,6BAA6B;AAAA,QAC7B,mBAAmB;AAAA,MACrB;AAAA,MACA,MAAM,KAAK,UAAU,GAAG;AAAA,IAC1B,CAAC;AAED,QAAI,SAAS,GAAI;AACjB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,YAAQ,MAAM,+CAA+C,MAAM,WAAW,OAAO,EAAE;AAAA,EACzF,SAAS,OAAO;AACd,YAAQ,MAAM,sDAAsD,KAAK;AAAA,EAC3E;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  // src/server/linkedin-conversions-api.ts
2
2
  import { createHash } from "crypto";
3
- import { fetch } from "../utils/fetch.mjs";
3
+ import { fetch } from "@shware/utils";
4
4
  import { getFirst } from "../utils/field.mjs";
5
5
  import { IGNORE_EVENTS } from "./ignore-events.mjs";
6
6
  async function sendEvents(accessToken, config, events, data = {}) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/linkedin-conversions-api.ts"],"sourcesContent":["/**\n * Conversions API Payload Builder: https://www.linkedin.com/developers/payload-builder\n * https://learn.microsoft.com/en-us/linkedin/marketing/conversions/conversions-overview?view=li-lms-2025-09\n */\nimport { createHash } from 'crypto';\nimport { fetch } from '../utils/fetch';\nimport { getFirst } from '../utils/field';\nimport { IGNORE_EVENTS } from './ignore-events';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\n\ntype UserIdType =\n | 'SHA256_EMAIL'\n | 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID'\n | 'ACXIOM_ID'\n | 'ORACLE_MOAT_ID';\n\nexport interface CreateLinkedinEventDTO {\n /**\n * For any conversion that you want to send through multiple methods, such as Insight Tag and\n * Conversions API, you must create a conversion rule for each data source (browser and server).\n * Then, you can implement a logic to pick up the eventId from the browser and send it with the\n * corresponding event from your server. If we receive an Insight Tag event and a Conversions API\n * event from the same account with the same eventId, we discard the Conversions API event and\n * count only the Insight Tag event in campaign reporting.\n */\n eventId?: string;\n\n /**\n * Replace <id> with the conversion ID extracted when creating the conversion rule\n * (e.g. urn:lla:llaPartnerConversion:<id>).\n */\n conversion: `urn:lla:llaPartnerConversion:${number}`;\n\n /** Epoch timestamp in milliseconds at which the conversion event happened. */\n conversionHappenedAt: number;\n conversionValue: { currencyCode: string; amount: string };\n user: {\n userIds: { idType: UserIdType; idValue: string }[];\n userInfo?: {\n firstName?: string;\n lastName?: string;\n companyName?: string;\n countryCode?: string;\n title?: string;\n };\n\n /**\n * The maximum supported size of the list is 1 at the moment. If the list contains multiple\n * values, only the first value will be used.\n */\n externalIds?: [string, ...string[]];\n\n /**\n * This is generated when users submit the Linkedin Lead-gen form\n * (e.g. urn:li:leadGenFormResponse:<id>).\n */\n lead?: `urn:li:leadGenFormResponse:${string}`;\n };\n}\n\nexport interface CreateMultipleLinkedinEventsDTO {\n elements: CreateLinkedinEventDTO[];\n}\n\nexport type LinkedinConversionConfig = Record<Lowercase<string>, number>;\n\nexport async function sendEvents(\n accessToken: string,\n config: LinkedinConversionConfig,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {}\n) {\n const eventNames = Object.keys(config);\n const address = getFirst(data.address);\n const userIds: { idType: UserIdType; idValue: string }[] = [];\n const externalIds: [string, ...string[]] | undefined = data.user_id ? [data.user_id] : undefined;\n const userInfo =\n address && address.first_name && address.last_name\n ? {\n firstName: address.first_name,\n lastName: address.last_name,\n countryCode: address.country,\n }\n : undefined;\n\n if (data.email) {\n const email = getFirst(data.email);\n if (email)\n userIds.push({\n idType: 'SHA256_EMAIL',\n idValue: createHash('sha256').update(email).digest('hex'),\n });\n }\n\n const dto: CreateMultipleLinkedinEventsDTO = {\n elements: events\n .filter((event) => eventNames.includes(event.name) && !IGNORE_EVENTS.includes(event.name))\n .map((event) => ({\n eventId: event.id,\n conversion: `urn:lla:llaPartnerConversion:${config[event.name]}`,\n conversionHappenedAt: Date.now(),\n conversionValue: {\n currencyCode: event.properties?.currency?.toUpperCase() ?? 'USD',\n amount: event.properties?.value?.toString() ?? '0',\n },\n user: {\n userIds: event.tags.li_fat_id\n ? [\n {\n idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID',\n idValue: event.tags.li_fat_id,\n },\n ...userIds,\n ]\n : userIds,\n userInfo,\n externalIds,\n },\n })),\n };\n\n if (dto.elements.length === 0) return;\n try {\n const response = await fetch('https://api.linkedin.com/rest/conversionEvents', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'LinkedIn-Version': '202509',\n 'X-Restli-Protocol-Version': '2.0.0',\n 'X-RestLi-Method': 'BATCH_CREATE',\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 LinkedIn conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send LinkedIn conversion, network error:', error);\n }\n}\n"],"mappings":";AAIA,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AA2D9B,eAAsB,WACpB,aACA,QAEA,QACA,OAAyB,CAAC,GAC1B;AACA,QAAM,aAAa,OAAO,KAAK,MAAM;AACrC,QAAM,UAAU,SAAS,KAAK,OAAO;AACrC,QAAM,UAAqD,CAAC;AAC5D,QAAM,cAAiD,KAAK,UAAU,CAAC,KAAK,OAAO,IAAI;AACvF,QAAM,WACJ,WAAW,QAAQ,cAAc,QAAQ,YACrC;AAAA,IACE,WAAW,QAAQ;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,aAAa,QAAQ;AAAA,EACvB,IACA;AAEN,MAAI,KAAK,OAAO;AACd,UAAM,QAAQ,SAAS,KAAK,KAAK;AACjC,QAAI;AACF,cAAQ,KAAK;AAAA,QACX,QAAQ;AAAA,QACR,SAAS,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAAA,MAC1D,CAAC;AAAA,EACL;AAEA,QAAM,MAAuC;AAAA,IAC3C,UAAU,OACP,OAAO,CAAC,UAAU,WAAW,SAAS,MAAM,IAAI,KAAK,CAAC,cAAc,SAAS,MAAM,IAAI,CAAC,EACxF,IAAI,CAAC,UAAO;AAlGnB;AAkGuB;AAAA,QACf,SAAS,MAAM;AAAA,QACf,YAAY,gCAAgC,OAAO,MAAM,IAAI,CAAC;AAAA,QAC9D,sBAAsB,KAAK,IAAI;AAAA,QAC/B,iBAAiB;AAAA,UACf,gBAAc,iBAAM,eAAN,mBAAkB,aAAlB,mBAA4B,kBAAiB;AAAA,UAC3D,UAAQ,iBAAM,eAAN,mBAAkB,UAAlB,mBAAyB,eAAc;AAAA,QACjD;AAAA,QACA,MAAM;AAAA,UACJ,SAAS,MAAM,KAAK,YAChB;AAAA,YACE;AAAA,cACE,QAAQ;AAAA,cACR,SAAS,MAAM,KAAK;AAAA,YACtB;AAAA,YACA,GAAG;AAAA,UACL,IACA;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,KAAE;AAAA,EACN;AAEA,MAAI,IAAI,SAAS,WAAW,EAAG;AAC/B,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,kDAAkD;AAAA,MAC7E,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,WAAW;AAAA,QACpC,gBAAgB;AAAA,QAChB,oBAAoB;AAAA,QACpB,6BAA6B;AAAA,QAC7B,mBAAmB;AAAA,MACrB;AAAA,MACA,MAAM,KAAK,UAAU,GAAG;AAAA,IAC1B,CAAC;AAED,QAAI,SAAS,GAAI;AACjB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,YAAQ,MAAM,+CAA+C,MAAM,WAAW,OAAO,EAAE;AAAA,EACzF,SAAS,OAAO;AACd,YAAQ,MAAM,sDAAsD,KAAK;AAAA,EAC3E;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/server/linkedin-conversions-api.ts"],"sourcesContent":["/**\n * Conversions API Payload Builder: https://www.linkedin.com/developers/payload-builder\n * https://learn.microsoft.com/en-us/linkedin/marketing/conversions/conversions-overview?view=li-lms-2025-09\n */\nimport { createHash } from 'crypto';\nimport { fetch } from '@shware/utils';\nimport { getFirst } from '../utils/field';\nimport { IGNORE_EVENTS } from './ignore-events';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\n\ntype UserIdType =\n | 'SHA256_EMAIL'\n | 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID'\n | 'ACXIOM_ID'\n | 'ORACLE_MOAT_ID';\n\nexport interface CreateLinkedinEventDTO {\n /**\n * For any conversion that you want to send through multiple methods, such as Insight Tag and\n * Conversions API, you must create a conversion rule for each data source (browser and server).\n * Then, you can implement a logic to pick up the eventId from the browser and send it with the\n * corresponding event from your server. If we receive an Insight Tag event and a Conversions API\n * event from the same account with the same eventId, we discard the Conversions API event and\n * count only the Insight Tag event in campaign reporting.\n */\n eventId?: string;\n\n /**\n * Replace <id> with the conversion ID extracted when creating the conversion rule\n * (e.g. urn:lla:llaPartnerConversion:<id>).\n */\n conversion: `urn:lla:llaPartnerConversion:${number}`;\n\n /** Epoch timestamp in milliseconds at which the conversion event happened. */\n conversionHappenedAt: number;\n conversionValue: { currencyCode: string; amount: string };\n user: {\n userIds: { idType: UserIdType; idValue: string }[];\n userInfo?: {\n firstName?: string;\n lastName?: string;\n companyName?: string;\n countryCode?: string;\n title?: string;\n };\n\n /**\n * The maximum supported size of the list is 1 at the moment. If the list contains multiple\n * values, only the first value will be used.\n */\n externalIds?: [string, ...string[]];\n\n /**\n * This is generated when users submit the Linkedin Lead-gen form\n * (e.g. urn:li:leadGenFormResponse:<id>).\n */\n lead?: `urn:li:leadGenFormResponse:${string}`;\n };\n}\n\nexport interface CreateMultipleLinkedinEventsDTO {\n elements: CreateLinkedinEventDTO[];\n}\n\nexport type LinkedinConversionConfig = Record<Lowercase<string>, number>;\n\nexport async function sendEvents(\n accessToken: string,\n config: LinkedinConversionConfig,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {}\n) {\n const eventNames = Object.keys(config);\n const address = getFirst(data.address);\n const userIds: { idType: UserIdType; idValue: string }[] = [];\n const externalIds: [string, ...string[]] | undefined = data.user_id ? [data.user_id] : undefined;\n const userInfo =\n address && address.first_name && address.last_name\n ? {\n firstName: address.first_name,\n lastName: address.last_name,\n countryCode: address.country,\n }\n : undefined;\n\n if (data.email) {\n const email = getFirst(data.email);\n if (email)\n userIds.push({\n idType: 'SHA256_EMAIL',\n idValue: createHash('sha256').update(email).digest('hex'),\n });\n }\n\n const dto: CreateMultipleLinkedinEventsDTO = {\n elements: events\n .filter((event) => eventNames.includes(event.name) && !IGNORE_EVENTS.includes(event.name))\n .map((event) => ({\n eventId: event.id,\n conversion: `urn:lla:llaPartnerConversion:${config[event.name]}`,\n conversionHappenedAt: Date.now(),\n conversionValue: {\n currencyCode: event.properties?.currency?.toUpperCase() ?? 'USD',\n amount: event.properties?.value?.toString() ?? '0',\n },\n user: {\n userIds: event.tags.li_fat_id\n ? [\n {\n idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID',\n idValue: event.tags.li_fat_id,\n },\n ...userIds,\n ]\n : userIds,\n userInfo,\n externalIds,\n },\n })),\n };\n\n if (dto.elements.length === 0) return;\n try {\n const response = await fetch('https://api.linkedin.com/rest/conversionEvents', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'LinkedIn-Version': '202509',\n 'X-Restli-Protocol-Version': '2.0.0',\n 'X-RestLi-Method': 'BATCH_CREATE',\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 LinkedIn conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send LinkedIn conversion, network error:', error);\n }\n}\n"],"mappings":";AAIA,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AA2D9B,eAAsB,WACpB,aACA,QAEA,QACA,OAAyB,CAAC,GAC1B;AACA,QAAM,aAAa,OAAO,KAAK,MAAM;AACrC,QAAM,UAAU,SAAS,KAAK,OAAO;AACrC,QAAM,UAAqD,CAAC;AAC5D,QAAM,cAAiD,KAAK,UAAU,CAAC,KAAK,OAAO,IAAI;AACvF,QAAM,WACJ,WAAW,QAAQ,cAAc,QAAQ,YACrC;AAAA,IACE,WAAW,QAAQ;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,aAAa,QAAQ;AAAA,EACvB,IACA;AAEN,MAAI,KAAK,OAAO;AACd,UAAM,QAAQ,SAAS,KAAK,KAAK;AACjC,QAAI;AACF,cAAQ,KAAK;AAAA,QACX,QAAQ;AAAA,QACR,SAAS,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAAA,MAC1D,CAAC;AAAA,EACL;AAEA,QAAM,MAAuC;AAAA,IAC3C,UAAU,OACP,OAAO,CAAC,UAAU,WAAW,SAAS,MAAM,IAAI,KAAK,CAAC,cAAc,SAAS,MAAM,IAAI,CAAC,EACxF,IAAI,CAAC,UAAO;AAlGnB;AAkGuB;AAAA,QACf,SAAS,MAAM;AAAA,QACf,YAAY,gCAAgC,OAAO,MAAM,IAAI,CAAC;AAAA,QAC9D,sBAAsB,KAAK,IAAI;AAAA,QAC/B,iBAAiB;AAAA,UACf,gBAAc,iBAAM,eAAN,mBAAkB,aAAlB,mBAA4B,kBAAiB;AAAA,UAC3D,UAAQ,iBAAM,eAAN,mBAAkB,UAAlB,mBAAyB,eAAc;AAAA,QACjD;AAAA,QACA,MAAM;AAAA,UACJ,SAAS,MAAM,KAAK,YAChB;AAAA,YACE;AAAA,cACE,QAAQ;AAAA,cACR,SAAS,MAAM,KAAK;AAAA,YACtB;AAAA,YACA,GAAG;AAAA,UACL,IACA;AAAA,UACJ;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,KAAE;AAAA,EACN;AAEA,MAAI,IAAI,SAAS,WAAW,EAAG;AAC/B,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,kDAAkD;AAAA,MAC7E,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,WAAW;AAAA,QACpC,gBAAgB;AAAA,QAChB,oBAAoB;AAAA,QACpB,6BAA6B;AAAA,QAC7B,mBAAmB;AAAA,MACrB;AAAA,MACA,MAAM,KAAK,UAAU,GAAG;AAAA,IAC1B,CAAC;AAED,QAAI,SAAS,GAAI;AACjB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,YAAQ,MAAM,+CAA+C,MAAM,WAAW,OAAO,EAAE;AAAA,EACzF,SAAS,OAAO;AACd,YAAQ,MAAM,sDAAsD,KAAK;AAAA,EAC3E;AACF;","names":[]}
@@ -24,8 +24,8 @@ __export(reddit_conversions_api_exports, {
24
24
  sendEvents: () => sendEvents
25
25
  });
26
26
  module.exports = __toCommonJS(reddit_conversions_api_exports);
27
+ var import_utils = require("@shware/utils");
27
28
  var import_rdt = require("../track/rdt.cjs");
28
- var import_fetch = require("../utils/fetch.cjs");
29
29
  var import_field = require("../utils/field.cjs");
30
30
  var import_ignore_events = require("./ignore-events.cjs");
31
31
  function getServerEvent(event, data) {
@@ -68,7 +68,7 @@ async function sendEvents(accessToken, pixelId, events, data = {}, testId) {
68
68
  };
69
69
  if (dto.data.events.length === 0) return;
70
70
  try {
71
- const response = await (0, import_fetch.fetch)(
71
+ const response = await (0, import_utils.fetch)(
72
72
  `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`,
73
73
  {
74
74
  method: "POST",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport { fetch } from '../utils/fetch';\nimport { getFirst } from '../utils/field';\nimport { IGNORE_EVENTS } from './ignore-events';\nimport type { ServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\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 // eslint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData\n): RedditEvent {\n const { id, name, properties, tags } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n\n return {\n click_id: tags.rdt_cid,\n event_at: Date.now(),\n action_source: tags.source === 'web' ? 'WEBSITE' : tags.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: tags.platform === 'ios' ? tags.advertising_id : undefined,\n aaid: tags.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 // eslint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n testId?: string\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORE_EVENTS.includes(event.name))\n .map((event) => getServerEvent(event, data)),\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAoD;AACpD,mBAAsB;AACtB,mBAAyB;AACzB,2BAA8B;AAwEvB,SAAS,eAEd,OACA,MACa;AACb,QAAM,EAAE,IAAI,MAAM,YAAY,KAAK,IAAI;AACvC,QAAM,CAAC,MAAM,MAAM,QAAI,wBAAY,MAAM,YAAY,EAAE;AAEvD,SAAO;AAAA,IACL,UAAU,KAAK;AAAA,IACf,UAAU,KAAK,IAAI;AAAA,IACnB,eAAe,KAAK,WAAW,QAAQ,YAAY,KAAK,WAAW,QAAQ,QAAQ;AAAA,IACnF,MAAM;AAAA,MACJ,eAAe,SAAS,WAAW,eAAW,mCAAuB,IAAI;AAAA,MACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB;AAAA,IAClE;AAAA,IACA,UAAU;AAAA,MACR,eAAe;AAAA,MACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B;AAAA,MACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP;AAAA,MACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,MAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP;AAAA,IACR;AAAA,IACA,MAAM;AAAA,MACJ,WAAO,uBAAS,KAAK,KAAK;AAAA,MAC1B,aAAa,KAAK;AAAA,MAClB,YAAY,KAAK;AAAA,MACjB,kBAAc,uBAAS,KAAK,YAAY;AAAA,MACxC,YAAY,KAAK;AAAA,MACjB,MAAM,KAAK,aAAa,QAAQ,KAAK,iBAAiB;AAAA,MACtD,MAAM,KAAK,aAAa,YAAY,KAAK,iBAAiB;AAAA,MAC1D,MAAM,KAAK;AAAA,MACX,mBACE,KAAK,gBAAgB,KAAK,gBACtB,EAAE,OAAO,KAAK,cAAc,QAAQ,KAAK,cAAc,IACvD;AAAA,IACR;AAAA,EACF;AACF;AAEA,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA;AACA,QAAM,MAA4B;AAAA,IAChC,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,QAAQ,OACL,OAAO,CAAC,UAAU,CAAC,mCAAc,SAAS,MAAM,IAAI,CAAC,EACrD,IAAI,CAAC,UAAU,eAAe,OAAO,IAAI,CAAC;AAAA,IAC/C;AAAA,EACF;AAEA,MAAI,IAAI,KAAK,OAAO,WAAW,EAAG;AAElC,MAAI;AACF,UAAM,WAAW,UAAM;AAAA,MACrB,4CAA4C,OAAO;AAAA,MACnD;AAAA,QACE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,QAAQ;AAAA,UACR,eAAe,UAAU,WAAW;AAAA,QACtC;AAAA,QACA,MAAM,KAAK,UAAU,GAAG;AAAA,MAC1B;AAAA,IACF;AACA,QAAI,SAAS,GAAI;AACjB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,YAAQ,MAAM,6CAA6C,MAAM,WAAW,OAAO,EAAE;AAAA,EACvF,SAAS,OAAO;AACd,YAAQ,MAAM,oDAAoD,KAAK;AAAA,EACzE;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport { getFirst } from '../utils/field';\nimport { IGNORE_EVENTS } from './ignore-events';\nimport type { ServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\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 // eslint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData\n): RedditEvent {\n const { id, name, properties, tags } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n\n return {\n click_id: tags.rdt_cid,\n event_at: Date.now(),\n action_source: tags.source === 'web' ? 'WEBSITE' : tags.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: tags.platform === 'ios' ? tags.advertising_id : undefined,\n aaid: tags.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 // eslint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n testId?: string\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORE_EVENTS.includes(event.name))\n .map((event) => getServerEvent(event, data)),\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAsB;AACtB,iBAAoD;AACpD,mBAAyB;AACzB,2BAA8B;AAwEvB,SAAS,eAEd,OACA,MACa;AACb,QAAM,EAAE,IAAI,MAAM,YAAY,KAAK,IAAI;AACvC,QAAM,CAAC,MAAM,MAAM,QAAI,wBAAY,MAAM,YAAY,EAAE;AAEvD,SAAO;AAAA,IACL,UAAU,KAAK;AAAA,IACf,UAAU,KAAK,IAAI;AAAA,IACnB,eAAe,KAAK,WAAW,QAAQ,YAAY,KAAK,WAAW,QAAQ,QAAQ;AAAA,IACnF,MAAM;AAAA,MACJ,eAAe,SAAS,WAAW,eAAW,mCAAuB,IAAI;AAAA,MACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB;AAAA,IAClE;AAAA,IACA,UAAU;AAAA,MACR,eAAe;AAAA,MACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B;AAAA,MACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP;AAAA,MACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,MAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP;AAAA,IACR;AAAA,IACA,MAAM;AAAA,MACJ,WAAO,uBAAS,KAAK,KAAK;AAAA,MAC1B,aAAa,KAAK;AAAA,MAClB,YAAY,KAAK;AAAA,MACjB,kBAAc,uBAAS,KAAK,YAAY;AAAA,MACxC,YAAY,KAAK;AAAA,MACjB,MAAM,KAAK,aAAa,QAAQ,KAAK,iBAAiB;AAAA,MACtD,MAAM,KAAK,aAAa,YAAY,KAAK,iBAAiB;AAAA,MAC1D,MAAM,KAAK;AAAA,MACX,mBACE,KAAK,gBAAgB,KAAK,gBACtB,EAAE,OAAO,KAAK,cAAc,QAAQ,KAAK,cAAc,IACvD;AAAA,IACR;AAAA,EACF;AACF;AAEA,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA;AACA,QAAM,MAA4B;AAAA,IAChC,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,QAAQ,OACL,OAAO,CAAC,UAAU,CAAC,mCAAc,SAAS,MAAM,IAAI,CAAC,EACrD,IAAI,CAAC,UAAU,eAAe,OAAO,IAAI,CAAC;AAAA,IAC/C;AAAA,EACF;AAEA,MAAI,IAAI,KAAK,OAAO,WAAW,EAAG;AAElC,MAAI;AACF,UAAM,WAAW,UAAM;AAAA,MACrB,4CAA4C,OAAO;AAAA,MACnD;AAAA,QACE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,QAAQ;AAAA,UACR,eAAe,UAAU,WAAW;AAAA,QACtC;AAAA,QACA,MAAM,KAAK,UAAU,GAAG;AAAA,MAC1B;AAAA,IACF;AACA,QAAI,SAAS,GAAI;AACjB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,YAAQ,MAAM,6CAA6C,MAAM,WAAW,OAAO,EAAE;AAAA,EACvF,SAAS,OAAO;AACd,YAAQ,MAAM,oDAAoD,KAAK;AAAA,EACzE;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  // src/server/reddit-conversions-api.ts
2
+ import { fetch } from "@shware/utils";
2
3
  import { mapRDTEvent, mapServerStandardEvent } from "../track/rdt.mjs";
3
- import { fetch } from "../utils/fetch.mjs";
4
4
  import { getFirst } from "../utils/field.mjs";
5
5
  import { IGNORE_EVENTS } from "./ignore-events.mjs";
6
6
  function getServerEvent(event, data) {