@shware/analytics 7.3.0 → 7.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/hooks/use-web-analytics.cjs +4 -3
- package/dist/hooks/use-web-analytics.cjs.map +1 -1
- package/dist/hooks/use-web-analytics.d.cts.map +1 -1
- package/dist/hooks/use-web-analytics.d.mts.map +1 -1
- package/dist/hooks/use-web-analytics.mjs +4 -3
- package/dist/hooks/use-web-analytics.mjs.map +1 -1
- package/dist/next/index.cjs +1 -1
- package/dist/next/index.mjs +1 -1
- package/dist/react-router/index.cjs +1 -1
- package/dist/react-router/index.mjs +1 -1
- package/dist/setup/session.cjs +8 -9
- package/dist/setup/session.cjs.map +1 -1
- package/dist/setup/session.d.cts +8 -6
- package/dist/setup/session.d.cts.map +1 -1
- package/dist/setup/session.d.mts +8 -6
- package/dist/setup/session.d.mts.map +1 -1
- package/dist/setup/session.mjs +8 -9
- package/dist/setup/session.mjs.map +1 -1
- package/dist/test/setup.cjs +43 -0
- package/dist/test/setup.cjs.map +1 -0
- package/dist/test/setup.d.cts +19 -0
- package/dist/test/setup.d.cts.map +1 -0
- package/dist/test/setup.d.mts +19 -0
- package/dist/test/setup.d.mts.map +1 -0
- package/dist/test/setup.mjs +40 -0
- package/dist/test/setup.mjs.map +1 -0
- package/dist/third-parties/meta-pixel.cjs +1 -1
- package/dist/third-parties/meta-pixel.cjs.map +1 -1
- package/dist/third-parties/meta-pixel.mjs +1 -1
- package/dist/third-parties/meta-pixel.mjs.map +1 -1
- package/dist/track/fbq.cjs +3 -3
- package/dist/track/fbq.cjs.map +1 -1
- package/dist/track/fbq.d.cts.map +1 -1
- package/dist/track/fbq.d.mts.map +1 -1
- package/dist/track/fbq.mjs +3 -3
- package/dist/track/fbq.mjs.map +1 -1
- package/dist/track/index.cjs +2 -2
- package/dist/track/index.cjs.map +1 -1
- package/dist/track/index.d.cts.map +1 -1
- package/dist/track/index.d.mts.map +1 -1
- package/dist/track/index.mjs +2 -2
- package/dist/track/index.mjs.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Shware Analytics SDK
|
|
2
2
|
|
|
3
|
+
## Client-side only
|
|
4
|
+
|
|
5
|
+
Everything this SDK holds — the session, the visitor, the tags cache, the config itself — lives in
|
|
6
|
+
module singletons, because there is exactly one visitor per browser or app. The package can be
|
|
7
|
+
imported on a server (nothing is constructed at module scope, so it evaluates fine under Cloudflare
|
|
8
|
+
Workers), but calling `track()` there shares one session and one visitor across every request the
|
|
9
|
+
isolate serves.
|
|
10
|
+
|
|
11
|
+
Server-side conversions go the other way: store the client's events, then hand them to
|
|
12
|
+
`@shware/analytics/server`, which takes a `TrackEvent` and forwards it to the Meta, Reddit, OpenAI
|
|
13
|
+
and LinkedIn conversions APIs.
|
|
14
|
+
|
|
3
15
|
## Config
|
|
4
16
|
|
|
5
17
|
layout.tsx
|
|
@@ -25,10 +25,12 @@ function sendUserEngagement(trigger) {
|
|
|
25
25
|
trigger
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
+
/** Whether the event was actually sent — a zero-engagement crossing reports nothing. */
|
|
28
29
|
function sendScroll() {
|
|
29
30
|
const engagement_time_msec = require_setup_session.getSession().flush();
|
|
30
|
-
if (engagement_time_msec <= 0) return;
|
|
31
|
+
if (engagement_time_msec <= 0) return false;
|
|
31
32
|
require_track_index.track("scroll", { engagement_time_msec });
|
|
33
|
+
return true;
|
|
32
34
|
}
|
|
33
35
|
function getScrollPercent() {
|
|
34
36
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
@@ -63,8 +65,7 @@ function useWebAnalytics(pathname) {
|
|
|
63
65
|
session.updateAccumulator();
|
|
64
66
|
if (hasSendScroll.current) return;
|
|
65
67
|
if (getScrollPercent() < 90) return;
|
|
66
|
-
hasSendScroll.current =
|
|
67
|
-
sendScroll();
|
|
68
|
+
hasSendScroll.current = sendScroll();
|
|
68
69
|
}, 500);
|
|
69
70
|
const checkpointEvents = [
|
|
70
71
|
"mousedown",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-analytics.cjs","names":["config","keys","getSession","usePrevious"],"sources":["../../src/hooks/use-web-analytics.ts"],"sourcesContent":["import { throttle } from '@shware/utils';\nimport { useEffect, useRef } from 'react';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstVisit(pathname: string) {\n if (config.storage.getItem(keys.first_visit_time)) return;\n track('first_visit', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n });\n config.storage.setItem(keys.first_visit_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement(trigger: 'pagehide' | 'visibilitychange') {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec, trigger });\n}\n\nfunction sendScroll() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec });\n}\n\nfunction getScrollPercent() {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return 0;\n return ((scrollTop + windowHeight) * 100) / docHeight;\n}\n\nfunction onPageHide() {\n getSession().pagehide();\n sendUserEngagement('pagehide');\n}\n\nfunction onVisibilityChange() {\n getSession().visibilitychange(document.visibilityState);\n if (document.visibilityState === 'hidden') {\n sendUserEngagement('visibilitychange');\n }\n}\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 useWebAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n // reset state when the pathname changes and send scroll when the user navigates to a new page\n const hasSendScroll = useRef(false);\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n // One lookup for the whole effect: `addEventListener` and its matching\n // `removeEventListener` have to be handed the very same function.\n const session = getSession();\n\n sendFirstVisit(pathname);\n\n const onScroll = throttle(() => {\n session.updateAccumulator();\n if (hasSendScroll.current) return;\n // only send scroll when the user has scrolled more than 90% of the page\n if (getScrollPercent() < 90) return;\n hasSendScroll.current =
|
|
1
|
+
{"version":3,"file":"use-web-analytics.cjs","names":["config","keys","getSession","usePrevious"],"sources":["../../src/hooks/use-web-analytics.ts"],"sourcesContent":["import { throttle } from '@shware/utils';\nimport { useEffect, useRef } from 'react';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstVisit(pathname: string) {\n if (config.storage.getItem(keys.first_visit_time)) return;\n track('first_visit', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n });\n config.storage.setItem(keys.first_visit_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement(trigger: 'pagehide' | 'visibilitychange') {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec, trigger });\n}\n\n/** Whether the event was actually sent — a zero-engagement crossing reports nothing. */\nfunction sendScroll(): boolean {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return false;\n track('scroll', { engagement_time_msec });\n return true;\n}\n\nfunction getScrollPercent() {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return 0;\n return ((scrollTop + windowHeight) * 100) / docHeight;\n}\n\nfunction onPageHide() {\n getSession().pagehide();\n sendUserEngagement('pagehide');\n}\n\nfunction onVisibilityChange() {\n getSession().visibilitychange(document.visibilityState);\n if (document.visibilityState === 'hidden') {\n sendUserEngagement('visibilitychange');\n }\n}\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 useWebAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n // reset state when the pathname changes and send scroll when the user navigates to a new page\n const hasSendScroll = useRef(false);\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n // One lookup for the whole effect: `addEventListener` and its matching\n // `removeEventListener` have to be handed the very same function.\n const session = getSession();\n\n sendFirstVisit(pathname);\n\n const onScroll = throttle(() => {\n session.updateAccumulator();\n if (hasSendScroll.current) return;\n // only send scroll when the user has scrolled more than 90% of the page\n if (getScrollPercent() < 90) return;\n // A crossing with no engaged time (a restored scroll position in an unfocused window) must\n // not consume the page's one shot: the flag is set only once the event is actually sent.\n hasSendScroll.current = sendScroll();\n }, 500);\n\n const checkpointEvents = ['mousedown', 'keydown', 'touchstart'];\n const checkpoint = throttle(session.updateAccumulator, 1000);\n\n window.addEventListener('focus', session.focus);\n window.addEventListener('blur', session.blur);\n window.addEventListener('scroll', onScroll, { passive: true });\n window.addEventListener('pageshow', session.pageshow);\n window.addEventListener('pagehide', onPageHide);\n document.addEventListener('visibilitychange', onVisibilityChange);\n\n // save checkpoint\n checkpointEvents.forEach((e) => {\n window.addEventListener(e, checkpoint, { passive: true, capture: true });\n });\n\n return () => {\n window.removeEventListener('focus', session.focus);\n window.removeEventListener('blur', session.blur);\n window.removeEventListener('scroll', onScroll);\n window.removeEventListener('pageshow', session.pageshow);\n window.removeEventListener('pagehide', onPageHide);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n checkpointEvents.forEach((e) => window.removeEventListener(e, checkpoint, { capture: true }));\n\n onScroll.cancel();\n checkpoint.cancel();\n };\n }, []);\n\n useEffect(() => {\n track('page_view', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n previous_page_path: prevPathname ?? undefined,\n engagement_time_msec: prevPathname ? getSession().flush() : undefined,\n });\n }, [pathname]);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-analytics.d.cts","names":[],"sources":["../../src/hooks/use-web-analytics.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"use-web-analytics.d.cts","names":[],"sources":["../../src/hooks/use-web-analytics.ts"],"mappings":";;;;;;iBA0DgB,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-analytics.d.mts","names":[],"sources":["../../src/hooks/use-web-analytics.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"use-web-analytics.d.mts","names":[],"sources":["../../src/hooks/use-web-analytics.ts"],"mappings":";;;;;;iBA0DgB,gBAAgB"}
|
|
@@ -24,10 +24,12 @@ function sendUserEngagement(trigger) {
|
|
|
24
24
|
trigger
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
+
/** Whether the event was actually sent — a zero-engagement crossing reports nothing. */
|
|
27
28
|
function sendScroll() {
|
|
28
29
|
const engagement_time_msec = getSession().flush();
|
|
29
|
-
if (engagement_time_msec <= 0) return;
|
|
30
|
+
if (engagement_time_msec <= 0) return false;
|
|
30
31
|
track("scroll", { engagement_time_msec });
|
|
32
|
+
return true;
|
|
31
33
|
}
|
|
32
34
|
function getScrollPercent() {
|
|
33
35
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
@@ -62,8 +64,7 @@ function useWebAnalytics(pathname) {
|
|
|
62
64
|
session.updateAccumulator();
|
|
63
65
|
if (hasSendScroll.current) return;
|
|
64
66
|
if (getScrollPercent() < 90) return;
|
|
65
|
-
hasSendScroll.current =
|
|
66
|
-
sendScroll();
|
|
67
|
+
hasSendScroll.current = sendScroll();
|
|
67
68
|
}, 500);
|
|
68
69
|
const checkpointEvents = [
|
|
69
70
|
"mousedown",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-analytics.mjs","names":[],"sources":["../../src/hooks/use-web-analytics.ts"],"sourcesContent":["import { throttle } from '@shware/utils';\nimport { useEffect, useRef } from 'react';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstVisit(pathname: string) {\n if (config.storage.getItem(keys.first_visit_time)) return;\n track('first_visit', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n });\n config.storage.setItem(keys.first_visit_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement(trigger: 'pagehide' | 'visibilitychange') {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec, trigger });\n}\n\nfunction sendScroll() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('scroll', { engagement_time_msec });\n}\n\nfunction getScrollPercent() {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return 0;\n return ((scrollTop + windowHeight) * 100) / docHeight;\n}\n\nfunction onPageHide() {\n getSession().pagehide();\n sendUserEngagement('pagehide');\n}\n\nfunction onVisibilityChange() {\n getSession().visibilitychange(document.visibilityState);\n if (document.visibilityState === 'hidden') {\n sendUserEngagement('visibilitychange');\n }\n}\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 useWebAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n // reset state when the pathname changes and send scroll when the user navigates to a new page\n const hasSendScroll = useRef(false);\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n // One lookup for the whole effect: `addEventListener` and its matching\n // `removeEventListener` have to be handed the very same function.\n const session = getSession();\n\n sendFirstVisit(pathname);\n\n const onScroll = throttle(() => {\n session.updateAccumulator();\n if (hasSendScroll.current) return;\n // only send scroll when the user has scrolled more than 90% of the page\n if (getScrollPercent() < 90) return;\n hasSendScroll.current =
|
|
1
|
+
{"version":3,"file":"use-web-analytics.mjs","names":[],"sources":["../../src/hooks/use-web-analytics.ts"],"sourcesContent":["import { throttle } from '@shware/utils';\nimport { useEffect, useRef } from 'react';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { sendBeacon, track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstVisit(pathname: string) {\n if (config.storage.getItem(keys.first_visit_time)) return;\n track('first_visit', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n });\n config.storage.setItem(keys.first_visit_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement(trigger: 'pagehide' | 'visibilitychange') {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n sendBeacon('user_engagement', { engagement_time_msec, trigger });\n}\n\n/** Whether the event was actually sent — a zero-engagement crossing reports nothing. */\nfunction sendScroll(): boolean {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return false;\n track('scroll', { engagement_time_msec });\n return true;\n}\n\nfunction getScrollPercent() {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n if (docHeight === 0) return 0;\n return ((scrollTop + windowHeight) * 100) / docHeight;\n}\n\nfunction onPageHide() {\n getSession().pagehide();\n sendUserEngagement('pagehide');\n}\n\nfunction onVisibilityChange() {\n getSession().visibilitychange(document.visibilityState);\n if (document.visibilityState === 'hidden') {\n sendUserEngagement('visibilitychange');\n }\n}\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 useWebAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n // reset state when the pathname changes and send scroll when the user navigates to a new page\n const hasSendScroll = useRef(false);\n useEffect(() => {\n hasSendScroll.current = false;\n }, [pathname]);\n\n useEffect(() => {\n // One lookup for the whole effect: `addEventListener` and its matching\n // `removeEventListener` have to be handed the very same function.\n const session = getSession();\n\n sendFirstVisit(pathname);\n\n const onScroll = throttle(() => {\n session.updateAccumulator();\n if (hasSendScroll.current) return;\n // only send scroll when the user has scrolled more than 90% of the page\n if (getScrollPercent() < 90) return;\n // A crossing with no engaged time (a restored scroll position in an unfocused window) must\n // not consume the page's one shot: the flag is set only once the event is actually sent.\n hasSendScroll.current = sendScroll();\n }, 500);\n\n const checkpointEvents = ['mousedown', 'keydown', 'touchstart'];\n const checkpoint = throttle(session.updateAccumulator, 1000);\n\n window.addEventListener('focus', session.focus);\n window.addEventListener('blur', session.blur);\n window.addEventListener('scroll', onScroll, { passive: true });\n window.addEventListener('pageshow', session.pageshow);\n window.addEventListener('pagehide', onPageHide);\n document.addEventListener('visibilitychange', onVisibilityChange);\n\n // save checkpoint\n checkpointEvents.forEach((e) => {\n window.addEventListener(e, checkpoint, { passive: true, capture: true });\n });\n\n return () => {\n window.removeEventListener('focus', session.focus);\n window.removeEventListener('blur', session.blur);\n window.removeEventListener('scroll', onScroll);\n window.removeEventListener('pageshow', session.pageshow);\n window.removeEventListener('pagehide', onPageHide);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n checkpointEvents.forEach((e) => window.removeEventListener(e, checkpoint, { capture: true }));\n\n onScroll.cancel();\n checkpoint.cancel();\n };\n }, []);\n\n useEffect(() => {\n track('page_view', {\n page_path: pathname,\n page_title: document.title,\n page_referrer: document.referrer,\n page_location: window.location.href,\n previous_page_path: prevPathname ?? undefined,\n engagement_time_msec: prevPathname ? getSession().flush() : undefined,\n });\n }, [pathname]);\n}\n"],"mappings":"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"}
|
package/dist/next/index.cjs
CHANGED
|
@@ -4,11 +4,11 @@ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
|
4
4
|
const require_track_index = require("../track/index.cjs");
|
|
5
5
|
const require_hooks_use_outbound_click_analytics = require("../hooks/use-outbound-click-analytics.cjs");
|
|
6
6
|
const require_hooks_use_web_analytics = require("../hooks/use-web-analytics.cjs");
|
|
7
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
7
|
let next_navigation_js = require("next/navigation.js");
|
|
9
8
|
let next_script_js = require("next/script.js");
|
|
10
9
|
next_script_js = require_runtime.__toESM(next_script_js, 1);
|
|
11
10
|
let next_web_vitals_js = require("next/web-vitals.js");
|
|
11
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
12
|
//#region src/next/index.tsx
|
|
13
13
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, hotjarId, redditPixelId, linkedInPartnerId, facebookAppId, reportWebVitals = true }) {
|
|
14
14
|
require_hooks_use_web_analytics.useWebAnalytics((0, next_navigation_js.usePathname)());
|
package/dist/next/index.mjs
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { track } from "../track/index.mjs";
|
|
3
3
|
import { useOutboundClickAnalytics } from "../hooks/use-outbound-click-analytics.mjs";
|
|
4
4
|
import { useWebAnalytics } from "../hooks/use-web-analytics.mjs";
|
|
5
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
5
|
import { usePathname } from "next/navigation.js";
|
|
7
6
|
import Script from "next/script.js";
|
|
8
7
|
import { useReportWebVitals } from "next/web-vitals.js";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/next/index.tsx
|
|
10
10
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, hotjarId, redditPixelId, linkedInPartnerId, facebookAppId, reportWebVitals = true }) {
|
|
11
11
|
useWebAnalytics(usePathname());
|
|
@@ -3,8 +3,8 @@ const require_track_index = require("../track/index.cjs");
|
|
|
3
3
|
const require_hooks_use_outbound_click_analytics = require("../hooks/use-outbound-click-analytics.cjs");
|
|
4
4
|
const require_hooks_use_report_web_vitals = require("../hooks/use-report-web-vitals.cjs");
|
|
5
5
|
const require_hooks_use_web_analytics = require("../hooks/use-web-analytics.cjs");
|
|
6
|
-
let react_router = require("react-router");
|
|
7
6
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
|
+
let react_router = require("react-router");
|
|
8
8
|
//#region src/react-router/index.tsx
|
|
9
9
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, redditPixelId, linkedInPartnerId, hotjarId, facebookAppId, reportWebVitals = true }) {
|
|
10
10
|
const { pathname } = (0, react_router.useLocation)();
|
|
@@ -2,8 +2,8 @@ import { track } from "../track/index.mjs";
|
|
|
2
2
|
import { useOutboundClickAnalytics } from "../hooks/use-outbound-click-analytics.mjs";
|
|
3
3
|
import { useReportWebVitals } from "../hooks/use-report-web-vitals.mjs";
|
|
4
4
|
import { useWebAnalytics } from "../hooks/use-web-analytics.mjs";
|
|
5
|
-
import { useLocation } from "react-router";
|
|
6
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useLocation } from "react-router";
|
|
7
7
|
//#region src/react-router/index.tsx
|
|
8
8
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, redditPixelId, linkedInPartnerId, hotjarId, facebookAppId, reportWebVitals = true }) {
|
|
9
9
|
const { pathname } = useLocation();
|
package/dist/setup/session.cjs
CHANGED
|
@@ -44,7 +44,7 @@ var Session = class {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
this.accumulatedTime = 0;
|
|
47
|
-
this.
|
|
47
|
+
this.lastTickTime = Date.now();
|
|
48
48
|
const session = {
|
|
49
49
|
id: (0, uuid.v7)(),
|
|
50
50
|
lastEventTime
|
|
@@ -66,18 +66,16 @@ var Session = class {
|
|
|
66
66
|
return stored.id;
|
|
67
67
|
};
|
|
68
68
|
this.isActive = () => this.active;
|
|
69
|
-
this.isVisible = () => this.visible;
|
|
70
|
-
this.isFocused = () => this.focused;
|
|
71
69
|
this.updateActive = (active) => {
|
|
72
70
|
this.active = active;
|
|
73
71
|
};
|
|
74
72
|
this.updateAccumulator = () => {
|
|
75
73
|
const now = Date.now();
|
|
76
74
|
if (this.focused && this.visible && this.active) {
|
|
77
|
-
const delta = now - this.
|
|
75
|
+
const delta = now - this.lastTickTime;
|
|
78
76
|
if (delta > 0 && delta < 18e5) this.accumulatedTime += delta;
|
|
79
77
|
}
|
|
80
|
-
this.
|
|
78
|
+
this.lastTickTime = now;
|
|
81
79
|
};
|
|
82
80
|
this.focus = () => {
|
|
83
81
|
this.updateAccumulator();
|
|
@@ -105,7 +103,7 @@ var Session = class {
|
|
|
105
103
|
this.accumulatedTime = 0;
|
|
106
104
|
return engagementTime;
|
|
107
105
|
};
|
|
108
|
-
this.
|
|
106
|
+
this.lastTickTime = Date.now();
|
|
109
107
|
this.accumulatedTime = 0;
|
|
110
108
|
this.active = true;
|
|
111
109
|
this.visible = typeof document !== "undefined" ? document.visibilityState === "visible" : true;
|
|
@@ -132,9 +130,10 @@ let session;
|
|
|
132
130
|
* isolate booted, taking down every route before a component rendered.
|
|
133
131
|
*
|
|
134
132
|
* Everything here is per-visitor browser or app state, so deferring the
|
|
135
|
-
* construction costs nothing and
|
|
136
|
-
*
|
|
137
|
-
*
|
|
133
|
+
* construction costs nothing and lets the server bundle be evaluated. It does
|
|
134
|
+
* not make any of it safe to use there: this instance, `cache` and `config` are
|
|
135
|
+
* module singletons, so calling `track()` on a server shares one session and one
|
|
136
|
+
* visitor across every request the isolate serves. Import it there; do not track.
|
|
138
137
|
*/
|
|
139
138
|
function getSession() {
|
|
140
139
|
return session ??= new Session();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.cjs","names":["config","keys"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { config } from './index';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\n/**\n * The part of a session that outlives the page: its identity, and the clock the timeout is\n * measured against. GA4 keeps these in its `_ga_<container>` cookie — alongside the engaged flag,\n * which arrives with `session_engaged` later — and rereads them for every event, which is what\n * makes a session survive a reload, span a whole multipage visit, and stay shared between two\n * tabs of the same site. Holding them in memory instead, as this did, starts a new session on\n * every full navigation and gives every tab one of its own.\n */\ninterface StoredSession {\n id: string;\n /** When the last event was sent. The timeout is measured from here, and only from here. */\n lastEventTime: number;\n}\n\n/** The session an event belongs to, and whether that event is the one that started it. */\ninterface SessionForEvent {\n id: string;\n started: boolean;\n}\n\n/**\n * `<version>.<id>.<lastEventTime>`, compact and cookie-safe rather than JSON: `{`, `\"`\n * and `,` all have to be percent-encoded in a cookie, and a host that wants one session across\n * its subdomains will hand `setupAnalytics` a cookie-backed `storage`, where this has to survive\n * unchanged. A uuidv7 contains no dots, so the record splits cleanly.\n *\n * The version guards a change the parser could not otherwise survive. A field appended to the end\n * does not need one — a short record simply leaves it undefined.\n */\nconst VERSION = '1';\n\nfunction readSession(): StoredSession | undefined {\n const raw = config.storage.getItem(keys.session);\n if (!raw) return undefined;\n\n const [version, id, lastEventTime] = raw.split('.');\n if (version !== VERSION || !id) return undefined;\n\n const parsed = { id, lastEventTime: Number(lastEventTime) };\n if (!Number.isFinite(parsed.lastEventTime)) return undefined;\n return parsed;\n}\n\nfunction writeSession({ id, lastEventTime }: StoredSession) {\n config.storage.setItem(keys.session, `${VERSION}.${id}.${lastEventTime}`);\n}\n\nclass Session {\n /**\n * Engagement is deliberately not stored: it is the time this page has accrued and not yet\n * reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie\n * carries the session, while the engagement timer lives and dies with the document.\n */\n private
|
|
1
|
+
{"version":3,"file":"session.cjs","names":["config","keys"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { config } from './index';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\n/**\n * The part of a session that outlives the page: its identity, and the clock the timeout is\n * measured against. GA4 keeps these in its `_ga_<container>` cookie — alongside the engaged flag,\n * which arrives with `session_engaged` later — and rereads them for every event, which is what\n * makes a session survive a reload, span a whole multipage visit, and stay shared between two\n * tabs of the same site. Holding them in memory instead, as this did, starts a new session on\n * every full navigation and gives every tab one of its own.\n */\ninterface StoredSession {\n id: string;\n /** When the last event was sent. The timeout is measured from here, and only from here. */\n lastEventTime: number;\n}\n\n/** The session an event belongs to, and whether that event is the one that started it. */\ninterface SessionForEvent {\n id: string;\n started: boolean;\n}\n\n/**\n * `<version>.<id>.<lastEventTime>`, compact and cookie-safe rather than JSON: `{`, `\"`\n * and `,` all have to be percent-encoded in a cookie, and a host that wants one session across\n * its subdomains will hand `setupAnalytics` a cookie-backed `storage`, where this has to survive\n * unchanged. A uuidv7 contains no dots, so the record splits cleanly.\n *\n * The version guards a change the parser could not otherwise survive. A field appended to the end\n * does not need one — a short record simply leaves it undefined.\n */\nconst VERSION = '1';\n\nfunction readSession(): StoredSession | undefined {\n const raw = config.storage.getItem(keys.session);\n if (!raw) return undefined;\n\n const [version, id, lastEventTime] = raw.split('.');\n if (version !== VERSION || !id) return undefined;\n\n const parsed = { id, lastEventTime: Number(lastEventTime) };\n if (!Number.isFinite(parsed.lastEventTime)) return undefined;\n return parsed;\n}\n\nfunction writeSession({ id, lastEventTime }: StoredSession) {\n config.storage.setItem(keys.session, `${VERSION}.${id}.${lastEventTime}`);\n}\n\nclass Session {\n /**\n * Engagement is deliberately not stored: it is the time this page has accrued and not yet\n * reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie\n * carries the session, while the engagement timer lives and dies with the document.\n *\n * `lastTickTime` is where the accumulator last settled up, rewritten on every tick — it is not\n * when the session began, and nothing here needs to know that.\n */\n private lastTickTime: number;\n private accumulatedTime: number;\n\n private active: boolean;\n private visible: boolean;\n private focused: boolean;\n\n constructor() {\n this.lastTickTime = Date.now();\n this.accumulatedTime = 0;\n\n this.active = true;\n this.visible = typeof document !== 'undefined' ? document.visibilityState === 'visible' : true;\n this.focused = typeof document !== 'undefined' ? document.hasFocus() : true;\n }\n\n /**\n * The session a batch of events belongs to: read the stored one, start a new one if it has\n * timed out or there is none, stamp it with when those events happened and write it back. Every\n * event goes through here, exactly as GA4 rereads and rewrites its cookie per event — caching\n * the session in memory would put the tabs back out of step with each other.\n */\n touch = (eventTime: number, lastEventTime = eventTime): SessionForEvent => {\n const stored = readSession();\n\n if (stored && eventTime - stored.lastEventTime <= SESSION_TIMEOUT) {\n // `Math.max`, because a batch that waited in a frozen tab can be older than what another\n // tab has since written, and a session must never be shortened by a late arrival.\n writeSession({ ...stored, lastEventTime: Math.max(stored.lastEventTime, lastEventTime) });\n return { id: stored.id, started: false };\n }\n\n // Engagement the previous session accrued but never reported dies with it rather than being\n // handed to its successor. GA4 does the same on `session_start`.\n this.accumulatedTime = 0;\n // Wall clock, not `eventTime`: this anchors the engagement timer for the page in front of the\n // visitor now, which a batch describing something that happened an hour ago says nothing about.\n this.lastTickTime = Date.now();\n\n const session: StoredSession = { id: uuidv7(), lastEventTime };\n writeSession(session);\n return { id: session.id, started: true };\n };\n\n /**\n * The id for an event that must not start a session — the `pagehide` beacon, which reports what\n * the session now ending accrued. A live session is extended, as any event extends it; one\n * already past its timeout still owns that engagement, so its id comes back without being\n * revived into a session no `session_start` ever announced.\n */\n extend = (): string => {\n const stored = readSession();\n if (!stored) return this.touch(Date.now()).id;\n\n const now = Date.now();\n if (now - stored.lastEventTime <= SESSION_TIMEOUT) {\n writeSession({ ...stored, lastEventTime: now });\n }\n return stored.id;\n };\n\n isActive = () => this.active;\n\n updateActive = (active: boolean) => {\n this.active = active;\n };\n\n updateAccumulator = () => {\n const now = Date.now();\n if (this.focused && this.visible && this.active) {\n const delta = now - this.lastTickTime;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n this.accumulatedTime += delta;\n }\n }\n this.lastTickTime = now;\n };\n\n focus = () => {\n this.updateAccumulator();\n this.focused = true;\n };\n\n blur = () => {\n this.updateAccumulator();\n this.focused = false;\n };\n\n pageshow = () => {\n this.updateAccumulator();\n this.active = true;\n };\n\n pagehide = () => {\n this.updateAccumulator();\n this.active = false;\n };\n\n visibilitychange = (state: DocumentVisibilityState) => {\n this.updateAccumulator();\n this.visible = state === 'visible';\n };\n\n flush = () => {\n this.updateAccumulator();\n const engagementTime = this.accumulatedTime;\n this.accumulatedTime = 0;\n return engagementTime;\n };\n}\n\nlet session: Session | undefined;\n\n/**\n * The session, built the first time something asks for it.\n *\n * Deliberately not a module-scope `new Session()`. The constructor calls\n * `uuidv7()`, `uuid` draws its bytes from `crypto.getRandomValues`, and\n * Cloudflare Workers reject that outside a request handler:\n *\n * Disallowed operation called within global scope. Asynchronous I/O\n * (ex: fetch() or connect()), setting a timeout, and generating random\n * values are not allowed within global scope.\n *\n * Module scope in a Worker is evaluated once when the isolate boots and is\n * then shared by every request that isolate serves, so a random value drawn\n * there would be the same for all of them — which is why the runtime refuses\n * to produce one. A host that server-renders on Workers reaches this module\n * through `track()` on the server as well, and the throw happened as the\n * isolate booted, taking down every route before a component rendered.\n *\n * Everything here is per-visitor browser or app state, so deferring the\n * construction costs nothing and lets the server bundle be evaluated. It does\n * not make any of it safe to use there: this instance, `cache` and `config` are\n * module singletons, so calling `track()` on a server shares one session and one\n * visitor across every request the isolate serves. Import it there; do not track.\n */\nexport function getSession() {\n return (session ??= new Session());\n}\n"],"mappings":";;;;;AAIA,MAAa,kBAAkB,OAAU;;;;;;;;;;AA+BzC,MAAM,UAAU;AAEhB,SAAS,cAAyC;CAChD,MAAM,MAAMA,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,OAAO;CAC/C,IAAI,CAAC,KAAK,OAAO,KAAA;CAEjB,MAAM,CAAC,SAAS,IAAI,iBAAiB,IAAI,MAAM,GAAG;CAClD,IAAI,YAAY,WAAW,CAAC,IAAI,OAAO,KAAA;CAEvC,MAAM,SAAS;EAAE;EAAI,eAAe,OAAO,aAAa;CAAE;CAC1D,IAAI,CAAC,OAAO,SAAS,OAAO,aAAa,GAAG,OAAO,KAAA;CACnD,OAAO;AACT;AAEA,SAAS,aAAa,EAAE,IAAI,iBAAgC;CAC1D,oBAAA,OAAO,QAAQ,QAAQA,0BAAAA,KAAK,SAAS,GAAG,QAAQ,GAAG,GAAG,GAAG,eAAe;AAC1E;AAEA,IAAM,UAAN,MAAc;CAgBZ,cAAc;EAeL,KAAA,SAAA,WAAmB,gBAAgB,cAA+B;GACzE,MAAM,SAAS,YAAY;GAE3B,IAAI,UAAU,YAAY,OAAO,iBAAA,MAAkC;IAGjE,aAAa;KAAE,GAAG;KAAQ,eAAe,KAAK,IAAI,OAAO,eAAe,aAAa;IAAE,CAAC;IACxF,OAAO;KAAE,IAAI,OAAO;KAAI,SAAS;IAAM;GACzC;GAIA,KAAK,kBAAkB;GAGvB,KAAK,eAAe,KAAK,IAAI;GAE7B,MAAM,UAAyB;IAAE,KAAA,GAAA,KAAA,GAAA,CAAW;IAAG;GAAc;GAC7D,aAAa,OAAO;GACpB,OAAO;IAAE,IAAI,QAAQ;IAAI,SAAS;GAAK;EACzC;EAQuB,KAAA,eAAA;GACrB,MAAM,SAAS,YAAY;GAC3B,IAAI,CAAC,QAAQ,OAAO,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC;GAE3C,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,MAAM,OAAO,iBAAA,MACf,aAAa;IAAE,GAAG;IAAQ,eAAe;GAAI,CAAC;GAEhD,OAAO,OAAO;EAChB;EAEiB,KAAA,iBAAA,KAAK;EAEN,KAAA,gBAAA,WAAoB;GAClC,KAAK,SAAS;EAChB;EAE0B,KAAA,0BAAA;GACxB,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,KAAK,WAAW,KAAK,WAAW,KAAK,QAAQ;IAC/C,MAAM,QAAQ,MAAM,KAAK;IACzB,IAAI,QAAQ,KAAK,QAAA,MACf,KAAK,mBAAmB;GAE5B;GACA,KAAK,eAAe;EACtB;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEa,KAAA,aAAA;GACX,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEoB,KAAA,oBAAA,UAAmC;GACrD,KAAK,kBAAkB;GACvB,KAAK,UAAU,UAAU;EAC3B;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,MAAM,iBAAiB,KAAK;GAC5B,KAAK,kBAAkB;GACvB,OAAO;EACT;EApGE,KAAK,eAAe,KAAK,IAAI;EAC7B,KAAK,kBAAkB;EAEvB,KAAK,SAAS;EACd,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,oBAAoB,YAAY;EAC1F,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,SAAS,IAAI;CACzE;AA+FF;AAEA,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;AA0BJ,SAAgB,aAAa;CAC3B,OAAQ,YAAY,IAAI,QAAQ;AAClC"}
|
package/dist/setup/session.d.cts
CHANGED
|
@@ -10,8 +10,11 @@ declare class Session {
|
|
|
10
10
|
* Engagement is deliberately not stored: it is the time this page has accrued and not yet
|
|
11
11
|
* reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie
|
|
12
12
|
* carries the session, while the engagement timer lives and dies with the document.
|
|
13
|
+
*
|
|
14
|
+
* `lastTickTime` is where the accumulator last settled up, rewritten on every tick — it is not
|
|
15
|
+
* when the session began, and nothing here needs to know that.
|
|
13
16
|
*/
|
|
14
|
-
private
|
|
17
|
+
private lastTickTime;
|
|
15
18
|
private accumulatedTime;
|
|
16
19
|
private active;
|
|
17
20
|
private visible;
|
|
@@ -32,8 +35,6 @@ declare class Session {
|
|
|
32
35
|
*/
|
|
33
36
|
extend: () => string;
|
|
34
37
|
isActive: () => boolean;
|
|
35
|
-
isVisible: () => boolean;
|
|
36
|
-
isFocused: () => boolean;
|
|
37
38
|
updateActive: (active: boolean) => void;
|
|
38
39
|
updateAccumulator: () => void;
|
|
39
40
|
focus: () => void;
|
|
@@ -62,9 +63,10 @@ declare class Session {
|
|
|
62
63
|
* isolate booted, taking down every route before a component rendered.
|
|
63
64
|
*
|
|
64
65
|
* Everything here is per-visitor browser or app state, so deferring the
|
|
65
|
-
* construction costs nothing and
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* construction costs nothing and lets the server bundle be evaluated. It does
|
|
67
|
+
* not make any of it safe to use there: this instance, `cache` and `config` are
|
|
68
|
+
* module singletons, so calling `track()` on a server shares one session and one
|
|
69
|
+
* visitor across every request the isolate serves. Import it there; do not track.
|
|
68
70
|
*/
|
|
69
71
|
declare function getSession(): Session;
|
|
70
72
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.cts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAIa;;UAiBH;EACR;EACA;;cA8BI
|
|
1
|
+
{"version":3,"file":"session.d.cts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAIa;;UAiBH;EACR;EACA;;cA8BI;;;;;;;;;UASI;UACA;UAEA;UACA;UACA;EAER;;;;;;;EAeA,QAAK,mBAAqB,2BAA8B;;;;;;;EA4BxD;EAWA;EAEA,eAAY;EAIZ;EAWA;EAKA;EAKA;EAKA;EAKA,mBAAgB,OAAW;EAK3B;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkCc,cAAU"}
|
package/dist/setup/session.d.mts
CHANGED
|
@@ -10,8 +10,11 @@ declare class Session {
|
|
|
10
10
|
* Engagement is deliberately not stored: it is the time this page has accrued and not yet
|
|
11
11
|
* reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie
|
|
12
12
|
* carries the session, while the engagement timer lives and dies with the document.
|
|
13
|
+
*
|
|
14
|
+
* `lastTickTime` is where the accumulator last settled up, rewritten on every tick — it is not
|
|
15
|
+
* when the session began, and nothing here needs to know that.
|
|
13
16
|
*/
|
|
14
|
-
private
|
|
17
|
+
private lastTickTime;
|
|
15
18
|
private accumulatedTime;
|
|
16
19
|
private active;
|
|
17
20
|
private visible;
|
|
@@ -32,8 +35,6 @@ declare class Session {
|
|
|
32
35
|
*/
|
|
33
36
|
extend: () => string;
|
|
34
37
|
isActive: () => boolean;
|
|
35
|
-
isVisible: () => boolean;
|
|
36
|
-
isFocused: () => boolean;
|
|
37
38
|
updateActive: (active: boolean) => void;
|
|
38
39
|
updateAccumulator: () => void;
|
|
39
40
|
focus: () => void;
|
|
@@ -62,9 +63,10 @@ declare class Session {
|
|
|
62
63
|
* isolate booted, taking down every route before a component rendered.
|
|
63
64
|
*
|
|
64
65
|
* Everything here is per-visitor browser or app state, so deferring the
|
|
65
|
-
* construction costs nothing and
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* construction costs nothing and lets the server bundle be evaluated. It does
|
|
67
|
+
* not make any of it safe to use there: this instance, `cache` and `config` are
|
|
68
|
+
* module singletons, so calling `track()` on a server shares one session and one
|
|
69
|
+
* visitor across every request the isolate serves. Import it there; do not track.
|
|
68
70
|
*/
|
|
69
71
|
declare function getSession(): Session;
|
|
70
72
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.mts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAIa;;UAiBH;EACR;EACA;;cA8BI
|
|
1
|
+
{"version":3,"file":"session.d.mts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAIa;;UAiBH;EACR;EACA;;cA8BI;;;;;;;;;UASI;UACA;UAEA;UACA;UACA;EAER;;;;;;;EAeA,QAAK,mBAAqB,2BAA8B;;;;;;;EA4BxD;EAWA;EAEA,eAAY;EAIZ;EAWA;EAKA;EAKA;EAKA;EAKA,mBAAgB,OAAW;EAK3B;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkCc,cAAU"}
|
package/dist/setup/session.mjs
CHANGED
|
@@ -43,7 +43,7 @@ var Session = class {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
this.accumulatedTime = 0;
|
|
46
|
-
this.
|
|
46
|
+
this.lastTickTime = Date.now();
|
|
47
47
|
const session = {
|
|
48
48
|
id: v7(),
|
|
49
49
|
lastEventTime
|
|
@@ -65,18 +65,16 @@ var Session = class {
|
|
|
65
65
|
return stored.id;
|
|
66
66
|
};
|
|
67
67
|
this.isActive = () => this.active;
|
|
68
|
-
this.isVisible = () => this.visible;
|
|
69
|
-
this.isFocused = () => this.focused;
|
|
70
68
|
this.updateActive = (active) => {
|
|
71
69
|
this.active = active;
|
|
72
70
|
};
|
|
73
71
|
this.updateAccumulator = () => {
|
|
74
72
|
const now = Date.now();
|
|
75
73
|
if (this.focused && this.visible && this.active) {
|
|
76
|
-
const delta = now - this.
|
|
74
|
+
const delta = now - this.lastTickTime;
|
|
77
75
|
if (delta > 0 && delta < 18e5) this.accumulatedTime += delta;
|
|
78
76
|
}
|
|
79
|
-
this.
|
|
77
|
+
this.lastTickTime = now;
|
|
80
78
|
};
|
|
81
79
|
this.focus = () => {
|
|
82
80
|
this.updateAccumulator();
|
|
@@ -104,7 +102,7 @@ var Session = class {
|
|
|
104
102
|
this.accumulatedTime = 0;
|
|
105
103
|
return engagementTime;
|
|
106
104
|
};
|
|
107
|
-
this.
|
|
105
|
+
this.lastTickTime = Date.now();
|
|
108
106
|
this.accumulatedTime = 0;
|
|
109
107
|
this.active = true;
|
|
110
108
|
this.visible = typeof document !== "undefined" ? document.visibilityState === "visible" : true;
|
|
@@ -131,9 +129,10 @@ let session;
|
|
|
131
129
|
* isolate booted, taking down every route before a component rendered.
|
|
132
130
|
*
|
|
133
131
|
* Everything here is per-visitor browser or app state, so deferring the
|
|
134
|
-
* construction costs nothing and
|
|
135
|
-
*
|
|
136
|
-
*
|
|
132
|
+
* construction costs nothing and lets the server bundle be evaluated. It does
|
|
133
|
+
* not make any of it safe to use there: this instance, `cache` and `config` are
|
|
134
|
+
* module singletons, so calling `track()` on a server shares one session and one
|
|
135
|
+
* visitor across every request the isolate serves. Import it there; do not track.
|
|
137
136
|
*/
|
|
138
137
|
function getSession() {
|
|
139
138
|
return session ??= new Session();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { config } from './index';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\n/**\n * The part of a session that outlives the page: its identity, and the clock the timeout is\n * measured against. GA4 keeps these in its `_ga_<container>` cookie — alongside the engaged flag,\n * which arrives with `session_engaged` later — and rereads them for every event, which is what\n * makes a session survive a reload, span a whole multipage visit, and stay shared between two\n * tabs of the same site. Holding them in memory instead, as this did, starts a new session on\n * every full navigation and gives every tab one of its own.\n */\ninterface StoredSession {\n id: string;\n /** When the last event was sent. The timeout is measured from here, and only from here. */\n lastEventTime: number;\n}\n\n/** The session an event belongs to, and whether that event is the one that started it. */\ninterface SessionForEvent {\n id: string;\n started: boolean;\n}\n\n/**\n * `<version>.<id>.<lastEventTime>`, compact and cookie-safe rather than JSON: `{`, `\"`\n * and `,` all have to be percent-encoded in a cookie, and a host that wants one session across\n * its subdomains will hand `setupAnalytics` a cookie-backed `storage`, where this has to survive\n * unchanged. A uuidv7 contains no dots, so the record splits cleanly.\n *\n * The version guards a change the parser could not otherwise survive. A field appended to the end\n * does not need one — a short record simply leaves it undefined.\n */\nconst VERSION = '1';\n\nfunction readSession(): StoredSession | undefined {\n const raw = config.storage.getItem(keys.session);\n if (!raw) return undefined;\n\n const [version, id, lastEventTime] = raw.split('.');\n if (version !== VERSION || !id) return undefined;\n\n const parsed = { id, lastEventTime: Number(lastEventTime) };\n if (!Number.isFinite(parsed.lastEventTime)) return undefined;\n return parsed;\n}\n\nfunction writeSession({ id, lastEventTime }: StoredSession) {\n config.storage.setItem(keys.session, `${VERSION}.${id}.${lastEventTime}`);\n}\n\nclass Session {\n /**\n * Engagement is deliberately not stored: it is the time this page has accrued and not yet\n * reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie\n * carries the session, while the engagement timer lives and dies with the document.\n */\n private
|
|
1
|
+
{"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\nimport { keys } from '../constants/storage';\nimport { config } from './index';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\n/**\n * The part of a session that outlives the page: its identity, and the clock the timeout is\n * measured against. GA4 keeps these in its `_ga_<container>` cookie — alongside the engaged flag,\n * which arrives with `session_engaged` later — and rereads them for every event, which is what\n * makes a session survive a reload, span a whole multipage visit, and stay shared between two\n * tabs of the same site. Holding them in memory instead, as this did, starts a new session on\n * every full navigation and gives every tab one of its own.\n */\ninterface StoredSession {\n id: string;\n /** When the last event was sent. The timeout is measured from here, and only from here. */\n lastEventTime: number;\n}\n\n/** The session an event belongs to, and whether that event is the one that started it. */\ninterface SessionForEvent {\n id: string;\n started: boolean;\n}\n\n/**\n * `<version>.<id>.<lastEventTime>`, compact and cookie-safe rather than JSON: `{`, `\"`\n * and `,` all have to be percent-encoded in a cookie, and a host that wants one session across\n * its subdomains will hand `setupAnalytics` a cookie-backed `storage`, where this has to survive\n * unchanged. A uuidv7 contains no dots, so the record splits cleanly.\n *\n * The version guards a change the parser could not otherwise survive. A field appended to the end\n * does not need one — a short record simply leaves it undefined.\n */\nconst VERSION = '1';\n\nfunction readSession(): StoredSession | undefined {\n const raw = config.storage.getItem(keys.session);\n if (!raw) return undefined;\n\n const [version, id, lastEventTime] = raw.split('.');\n if (version !== VERSION || !id) return undefined;\n\n const parsed = { id, lastEventTime: Number(lastEventTime) };\n if (!Number.isFinite(parsed.lastEventTime)) return undefined;\n return parsed;\n}\n\nfunction writeSession({ id, lastEventTime }: StoredSession) {\n config.storage.setItem(keys.session, `${VERSION}.${id}.${lastEventTime}`);\n}\n\nclass Session {\n /**\n * Engagement is deliberately not stored: it is the time this page has accrued and not yet\n * reported, so it belongs to the page, not to the session. GA4 draws the same line — its cookie\n * carries the session, while the engagement timer lives and dies with the document.\n *\n * `lastTickTime` is where the accumulator last settled up, rewritten on every tick — it is not\n * when the session began, and nothing here needs to know that.\n */\n private lastTickTime: number;\n private accumulatedTime: number;\n\n private active: boolean;\n private visible: boolean;\n private focused: boolean;\n\n constructor() {\n this.lastTickTime = Date.now();\n this.accumulatedTime = 0;\n\n this.active = true;\n this.visible = typeof document !== 'undefined' ? document.visibilityState === 'visible' : true;\n this.focused = typeof document !== 'undefined' ? document.hasFocus() : true;\n }\n\n /**\n * The session a batch of events belongs to: read the stored one, start a new one if it has\n * timed out or there is none, stamp it with when those events happened and write it back. Every\n * event goes through here, exactly as GA4 rereads and rewrites its cookie per event — caching\n * the session in memory would put the tabs back out of step with each other.\n */\n touch = (eventTime: number, lastEventTime = eventTime): SessionForEvent => {\n const stored = readSession();\n\n if (stored && eventTime - stored.lastEventTime <= SESSION_TIMEOUT) {\n // `Math.max`, because a batch that waited in a frozen tab can be older than what another\n // tab has since written, and a session must never be shortened by a late arrival.\n writeSession({ ...stored, lastEventTime: Math.max(stored.lastEventTime, lastEventTime) });\n return { id: stored.id, started: false };\n }\n\n // Engagement the previous session accrued but never reported dies with it rather than being\n // handed to its successor. GA4 does the same on `session_start`.\n this.accumulatedTime = 0;\n // Wall clock, not `eventTime`: this anchors the engagement timer for the page in front of the\n // visitor now, which a batch describing something that happened an hour ago says nothing about.\n this.lastTickTime = Date.now();\n\n const session: StoredSession = { id: uuidv7(), lastEventTime };\n writeSession(session);\n return { id: session.id, started: true };\n };\n\n /**\n * The id for an event that must not start a session — the `pagehide` beacon, which reports what\n * the session now ending accrued. A live session is extended, as any event extends it; one\n * already past its timeout still owns that engagement, so its id comes back without being\n * revived into a session no `session_start` ever announced.\n */\n extend = (): string => {\n const stored = readSession();\n if (!stored) return this.touch(Date.now()).id;\n\n const now = Date.now();\n if (now - stored.lastEventTime <= SESSION_TIMEOUT) {\n writeSession({ ...stored, lastEventTime: now });\n }\n return stored.id;\n };\n\n isActive = () => this.active;\n\n updateActive = (active: boolean) => {\n this.active = active;\n };\n\n updateAccumulator = () => {\n const now = Date.now();\n if (this.focused && this.visible && this.active) {\n const delta = now - this.lastTickTime;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n this.accumulatedTime += delta;\n }\n }\n this.lastTickTime = now;\n };\n\n focus = () => {\n this.updateAccumulator();\n this.focused = true;\n };\n\n blur = () => {\n this.updateAccumulator();\n this.focused = false;\n };\n\n pageshow = () => {\n this.updateAccumulator();\n this.active = true;\n };\n\n pagehide = () => {\n this.updateAccumulator();\n this.active = false;\n };\n\n visibilitychange = (state: DocumentVisibilityState) => {\n this.updateAccumulator();\n this.visible = state === 'visible';\n };\n\n flush = () => {\n this.updateAccumulator();\n const engagementTime = this.accumulatedTime;\n this.accumulatedTime = 0;\n return engagementTime;\n };\n}\n\nlet session: Session | undefined;\n\n/**\n * The session, built the first time something asks for it.\n *\n * Deliberately not a module-scope `new Session()`. The constructor calls\n * `uuidv7()`, `uuid` draws its bytes from `crypto.getRandomValues`, and\n * Cloudflare Workers reject that outside a request handler:\n *\n * Disallowed operation called within global scope. Asynchronous I/O\n * (ex: fetch() or connect()), setting a timeout, and generating random\n * values are not allowed within global scope.\n *\n * Module scope in a Worker is evaluated once when the isolate boots and is\n * then shared by every request that isolate serves, so a random value drawn\n * there would be the same for all of them — which is why the runtime refuses\n * to produce one. A host that server-renders on Workers reaches this module\n * through `track()` on the server as well, and the throw happened as the\n * isolate booted, taking down every route before a component rendered.\n *\n * Everything here is per-visitor browser or app state, so deferring the\n * construction costs nothing and lets the server bundle be evaluated. It does\n * not make any of it safe to use there: this instance, `cache` and `config` are\n * module singletons, so calling `track()` on a server shares one session and one\n * visitor across every request the isolate serves. Import it there; do not track.\n */\nexport function getSession() {\n return (session ??= new Session());\n}\n"],"mappings":";;;;AAIA,MAAa,kBAAkB,OAAU;;;;;;;;;;AA+BzC,MAAM,UAAU;AAEhB,SAAS,cAAyC;CAChD,MAAM,MAAM,OAAO,QAAQ,QAAQ,KAAK,OAAO;CAC/C,IAAI,CAAC,KAAK,OAAO,KAAA;CAEjB,MAAM,CAAC,SAAS,IAAI,iBAAiB,IAAI,MAAM,GAAG;CAClD,IAAI,YAAY,WAAW,CAAC,IAAI,OAAO,KAAA;CAEvC,MAAM,SAAS;EAAE;EAAI,eAAe,OAAO,aAAa;CAAE;CAC1D,IAAI,CAAC,OAAO,SAAS,OAAO,aAAa,GAAG,OAAO,KAAA;CACnD,OAAO;AACT;AAEA,SAAS,aAAa,EAAE,IAAI,iBAAgC;CAC1D,OAAO,QAAQ,QAAQ,KAAK,SAAS,GAAG,QAAQ,GAAG,GAAG,GAAG,eAAe;AAC1E;AAEA,IAAM,UAAN,MAAc;CAgBZ,cAAc;EAeL,KAAA,SAAA,WAAmB,gBAAgB,cAA+B;GACzE,MAAM,SAAS,YAAY;GAE3B,IAAI,UAAU,YAAY,OAAO,iBAAA,MAAkC;IAGjE,aAAa;KAAE,GAAG;KAAQ,eAAe,KAAK,IAAI,OAAO,eAAe,aAAa;IAAE,CAAC;IACxF,OAAO;KAAE,IAAI,OAAO;KAAI,SAAS;IAAM;GACzC;GAIA,KAAK,kBAAkB;GAGvB,KAAK,eAAe,KAAK,IAAI;GAE7B,MAAM,UAAyB;IAAE,IAAIA,GAAO;IAAG;GAAc;GAC7D,aAAa,OAAO;GACpB,OAAO;IAAE,IAAI,QAAQ;IAAI,SAAS;GAAK;EACzC;EAQuB,KAAA,eAAA;GACrB,MAAM,SAAS,YAAY;GAC3B,IAAI,CAAC,QAAQ,OAAO,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC;GAE3C,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,MAAM,OAAO,iBAAA,MACf,aAAa;IAAE,GAAG;IAAQ,eAAe;GAAI,CAAC;GAEhD,OAAO,OAAO;EAChB;EAEiB,KAAA,iBAAA,KAAK;EAEN,KAAA,gBAAA,WAAoB;GAClC,KAAK,SAAS;EAChB;EAE0B,KAAA,0BAAA;GACxB,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,KAAK,WAAW,KAAK,WAAW,KAAK,QAAQ;IAC/C,MAAM,QAAQ,MAAM,KAAK;IACzB,IAAI,QAAQ,KAAK,QAAA,MACf,KAAK,mBAAmB;GAE5B;GACA,KAAK,eAAe;EACtB;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEa,KAAA,aAAA;GACX,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEoB,KAAA,oBAAA,UAAmC;GACrD,KAAK,kBAAkB;GACvB,KAAK,UAAU,UAAU;EAC3B;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,MAAM,iBAAiB,KAAK;GAC5B,KAAK,kBAAkB;GACvB,OAAO;EACT;EApGE,KAAK,eAAe,KAAK,IAAI;EAC7B,KAAK,kBAAkB;EAEvB,KAAK,SAAS;EACd,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,oBAAoB,YAAY;EAC1F,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,SAAS,IAAI;CACzE;AA+FF;AAEA,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;AA0BJ,SAAgB,aAAa;CAC3B,OAAQ,YAAY,IAAI,QAAQ;AAClC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/test/setup.ts
|
|
3
|
+
/**
|
|
4
|
+
* The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module
|
|
5
|
+
* scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with
|
|
6
|
+
* `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to
|
|
7
|
+
* `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.
|
|
8
|
+
*/
|
|
9
|
+
function memoryStorage(seed = {}) {
|
|
10
|
+
const map = new Map(Object.entries(seed));
|
|
11
|
+
return {
|
|
12
|
+
map,
|
|
13
|
+
getItem: (key) => map.get(key) ?? null,
|
|
14
|
+
setItem: (key, value) => {
|
|
15
|
+
map.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function baseOptions(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
release: "1.0.0",
|
|
22
|
+
storage: memoryStorage(),
|
|
23
|
+
endpoint: "https://api.test",
|
|
24
|
+
platform: "web",
|
|
25
|
+
environment: "production",
|
|
26
|
+
getTags: () => ({}),
|
|
27
|
+
getDeviceId: () => "device-1",
|
|
28
|
+
...overrides
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** A minimal ok Response whose json body is `data`. */
|
|
32
|
+
function jsonResponse(data, status = 200) {
|
|
33
|
+
return new Response(JSON.stringify(data), {
|
|
34
|
+
status,
|
|
35
|
+
headers: { "Content-Type": "application/json" }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
exports.baseOptions = baseOptions;
|
|
40
|
+
exports.jsonResponse = jsonResponse;
|
|
41
|
+
exports.memoryStorage = memoryStorage;
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=setup.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.cjs","names":[],"sources":["../../src/test/setup.ts"],"sourcesContent":["import type { Options } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\n/**\n * The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module\n * scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with\n * `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to\n * `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.\n */\nexport function memoryStorage(seed: Record<string, string> = {}) {\n const map = new Map(Object.entries(seed));\n return {\n map,\n getItem: (key: string) => map.get(key) ?? null,\n setItem: (key: string, value: string) => {\n map.set(key, value);\n },\n };\n}\n\nexport function baseOptions(overrides: Partial<Options> = {}): Options {\n return {\n release: '1.0.0',\n storage: memoryStorage(),\n endpoint: 'https://api.test',\n platform: 'web',\n environment: 'production',\n getTags: (): TrackTags => ({}),\n getDeviceId: () => 'device-1',\n ...overrides,\n };\n}\n\n/** A minimal ok Response whose json body is `data`. */\nexport function jsonResponse(data: unknown, status = 200) {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n}\n"],"mappings":";;;;;;;;AASA,SAAgB,cAAc,OAA+B,CAAC,GAAG;CAC/D,MAAM,MAAM,IAAI,IAAI,OAAO,QAAQ,IAAI,CAAC;CACxC,OAAO;EACL;EACA,UAAU,QAAgB,IAAI,IAAI,GAAG,KAAK;EAC1C,UAAU,KAAa,UAAkB;GACvC,IAAI,IAAI,KAAK,KAAK;EACpB;CACF;AACF;AAEA,SAAgB,YAAY,YAA8B,CAAC,GAAY;CACrE,OAAO;EACL,SAAS;EACT,SAAS,cAAc;EACvB,UAAU;EACV,UAAU;EACV,aAAa;EACb,gBAA2B,CAAC;EAC5B,mBAAmB;EACnB,GAAG;CACL;AACF;;AAGA,SAAgB,aAAa,MAAe,SAAS,KAAK;CACxD,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EACxC;EACA,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Options } from "../setup/index.cjs";
|
|
2
|
+
//#region src/test/setup.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module
|
|
5
|
+
* scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with
|
|
6
|
+
* `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to
|
|
7
|
+
* `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.
|
|
8
|
+
*/
|
|
9
|
+
declare function memoryStorage(seed?: Record<string, string>): {
|
|
10
|
+
map: Map<string, string>;
|
|
11
|
+
getItem: (key: string) => string | null;
|
|
12
|
+
setItem: (key: string, value: string) => void;
|
|
13
|
+
};
|
|
14
|
+
declare function baseOptions(overrides?: Partial<Options>): Options;
|
|
15
|
+
/** A minimal ok Response whose json body is `data`. */
|
|
16
|
+
declare function jsonResponse(data: unknown, status?: number): Response;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { baseOptions, jsonResponse, memoryStorage };
|
|
19
|
+
//# sourceMappingURL=setup.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.cts","names":[],"sources":["../../src/test/setup.ts"],"mappings":";;;;;;;;iBASgB,cAAc,OAAM;OAAA;EAIjB,UAAA;EACA,UAAA,aAAM;;iBAMT,YAAY,YAAW,QAAQ,WAAgB;;iBAc/C,aAAa,eAAe,kBAAY"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Options } from "../setup/index.mjs";
|
|
2
|
+
//#region src/test/setup.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module
|
|
5
|
+
* scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with
|
|
6
|
+
* `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to
|
|
7
|
+
* `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.
|
|
8
|
+
*/
|
|
9
|
+
declare function memoryStorage(seed?: Record<string, string>): {
|
|
10
|
+
map: Map<string, string>;
|
|
11
|
+
getItem: (key: string) => string | null;
|
|
12
|
+
setItem: (key: string, value: string) => void;
|
|
13
|
+
};
|
|
14
|
+
declare function baseOptions(overrides?: Partial<Options>): Options;
|
|
15
|
+
/** A minimal ok Response whose json body is `data`. */
|
|
16
|
+
declare function jsonResponse(data: unknown, status?: number): Response;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { baseOptions, jsonResponse, memoryStorage };
|
|
19
|
+
//# sourceMappingURL=setup.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.mts","names":[],"sources":["../../src/test/setup.ts"],"mappings":";;;;;;;;iBASgB,cAAc,OAAM;OAAA;EAIjB,UAAA;EACA,UAAA,aAAM;;iBAMT,YAAY,YAAW,QAAQ,WAAgB;;iBAc/C,aAAa,eAAe,kBAAY"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/test/setup.ts
|
|
2
|
+
/**
|
|
3
|
+
* The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module
|
|
4
|
+
* scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with
|
|
5
|
+
* `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to
|
|
6
|
+
* `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.
|
|
7
|
+
*/
|
|
8
|
+
function memoryStorage(seed = {}) {
|
|
9
|
+
const map = new Map(Object.entries(seed));
|
|
10
|
+
return {
|
|
11
|
+
map,
|
|
12
|
+
getItem: (key) => map.get(key) ?? null,
|
|
13
|
+
setItem: (key, value) => {
|
|
14
|
+
map.set(key, value);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function baseOptions(overrides = {}) {
|
|
19
|
+
return {
|
|
20
|
+
release: "1.0.0",
|
|
21
|
+
storage: memoryStorage(),
|
|
22
|
+
endpoint: "https://api.test",
|
|
23
|
+
platform: "web",
|
|
24
|
+
environment: "production",
|
|
25
|
+
getTags: () => ({}),
|
|
26
|
+
getDeviceId: () => "device-1",
|
|
27
|
+
...overrides
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** A minimal ok Response whose json body is `data`. */
|
|
31
|
+
function jsonResponse(data, status = 200) {
|
|
32
|
+
return new Response(JSON.stringify(data), {
|
|
33
|
+
status,
|
|
34
|
+
headers: { "Content-Type": "application/json" }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { baseOptions, jsonResponse, memoryStorage };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=setup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.mjs","names":[],"sources":["../../src/test/setup.ts"],"sourcesContent":["import type { Options } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\n/**\n * The SDK's state — `config`, `cache`, the session singleton, the track queue — lives in module\n * scope, exactly as it does in a browser. Tests therefore load the modules fresh per test with\n * `vi.resetModules()` and dynamic imports; this file only provides the pieces every test hands to\n * `setupAnalytics`, and is itself imported dynamically so it joins the same module graph.\n */\nexport function memoryStorage(seed: Record<string, string> = {}) {\n const map = new Map(Object.entries(seed));\n return {\n map,\n getItem: (key: string) => map.get(key) ?? null,\n setItem: (key: string, value: string) => {\n map.set(key, value);\n },\n };\n}\n\nexport function baseOptions(overrides: Partial<Options> = {}): Options {\n return {\n release: '1.0.0',\n storage: memoryStorage(),\n endpoint: 'https://api.test',\n platform: 'web',\n environment: 'production',\n getTags: (): TrackTags => ({}),\n getDeviceId: () => 'device-1',\n ...overrides,\n };\n}\n\n/** A minimal ok Response whose json body is `data`. */\nexport function jsonResponse(data: unknown, status = 200) {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n}\n"],"mappings":";;;;;;;AASA,SAAgB,cAAc,OAA+B,CAAC,GAAG;CAC/D,MAAM,MAAM,IAAI,IAAI,OAAO,QAAQ,IAAI,CAAC;CACxC,OAAO;EACL;EACA,UAAU,QAAgB,IAAI,IAAI,GAAG,KAAK;EAC1C,UAAU,KAAa,UAAkB;GACvC,IAAI,IAAI,KAAK,KAAK;EACpB;CACF;AACF;AAEA,SAAgB,YAAY,YAA8B,CAAC,GAAY;CACrE,OAAO;EACL,SAAS;EACT,SAAS,cAAc;EACvB,UAAU;EACV,UAAU;EACV,aAAa;EACb,gBAA2B,CAAC;EAC5B,mBAAmB;EACnB,GAAG;CACL;AACF;;AAGA,SAAgB,aAAa,MAAe,SAAS,KAAK;CACxD,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EACxC;EACA,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta-pixel.cjs","names":["mapFBEvent","getFirst"],"sources":["../../src/third-parties/meta-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { type FBQ, type PixelId, mapFBEvent } from '../track/fbq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\n\ndeclare global {\n interface Window {\n /** Undefined until the Meta Pixel script has loaded. */\n fbq?: FBQ['fbq'];\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\nexport function sendFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n event_id?: string\n) {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n if (metrics.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const options = { eventID: event_id };\n const [type, fbEventName, fbEventProperties] = mapFBEvent(name, properties);\n // The two branches are identical on purpose. `fbq` is overloaded per `type` — 'track' takes a\n // standard event name with its typed properties, 'trackCustom' an arbitrary string — and what\n // `mapFBEvent` returns is a union of both shapes. Narrowing `type` is what picks a single\n // overload; collapsing the branches leaves the call matching none of them.\n if (type === 'track') {\n window.fbq(type, fbEventName, fbEventProperties, options);\n } else {\n window.fbq(type, fbEventName, fbEventProperties, options);\n }\n}\n\nexport function setFBUser(pixelId: PixelId) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n\n const address = getFirst(user_data?.address);\n\n window.fbq('init', pixelId, {\n em: getFirst(user_data?.email),\n fn: address?.first_name,\n ln: address?.last_name,\n ph: getFirst(user_data?.phone_number),\n external_id: user_id,\n ct: address?.city,\n st: address?.
|
|
1
|
+
{"version":3,"file":"meta-pixel.cjs","names":["mapFBEvent","getFirst"],"sources":["../../src/third-parties/meta-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { type FBQ, type PixelId, mapFBEvent } from '../track/fbq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\n\ndeclare global {\n interface Window {\n /** Undefined until the Meta Pixel script has loaded. */\n fbq?: FBQ['fbq'];\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\nexport function sendFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n event_id?: string\n) {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n if (metrics.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const options = { eventID: event_id };\n const [type, fbEventName, fbEventProperties] = mapFBEvent(name, properties);\n // The two branches are identical on purpose. `fbq` is overloaded per `type` — 'track' takes a\n // standard event name with its typed properties, 'trackCustom' an arbitrary string — and what\n // `mapFBEvent` returns is a union of both shapes. Narrowing `type` is what picks a single\n // overload; collapsing the branches leaves the call matching none of them.\n if (type === 'track') {\n window.fbq(type, fbEventName, fbEventProperties, options);\n } else {\n window.fbq(type, fbEventName, fbEventProperties, options);\n }\n}\n\nexport function setFBUser(pixelId: PixelId) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n\n const address = getFirst(user_data?.address);\n\n window.fbq('init', pixelId, {\n em: getFirst(user_data?.email),\n fn: address?.first_name,\n ln: address?.last_name,\n ph: getFirst(user_data?.phone_number),\n external_id: user_id,\n ct: address?.city,\n // `st` is Meta's state/province field, not the street address.\n st: address?.region,\n zp: address?.postal_code,\n country: address?.country,\n });\n };\n}\n"],"mappings":";;;;AAYA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;AAE1D,SAAgB,YACd,MACA,YACA,UACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,KAAK;EAChD,QAAQ,KAAK,8BAA8B;EAC3C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,GAAG;CAC5B,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,UAAU,EAAE,SAAS,SAAS;CACpC,MAAM,CAAC,MAAM,aAAa,qBAAqBA,kBAAAA,WAAW,MAAM,UAAU;CAK1E,IAAI,SAAS,SACX,OAAO,IAAI,MAAM,aAAa,mBAAmB,OAAO;MAExD,OAAO,IAAI,MAAM,aAAa,mBAAmB,OAAO;AAE5D;AAEA,SAAgB,UAAU,SAAkB;CAC1C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,KAAK;GAChD,QAAQ,KAAK,8BAA8B;GAC3C;EACF;EAEA,MAAM,UAAUC,oBAAAA,SAAS,WAAW,OAAO;EAE3C,OAAO,IAAI,QAAQ,SAAS;GAC1B,IAAIA,oBAAAA,SAAS,WAAW,KAAK;GAC7B,IAAI,SAAS;GACb,IAAI,SAAS;GACb,IAAIA,oBAAAA,SAAS,WAAW,YAAY;GACpC,aAAa;GACb,IAAI,SAAS;GAEb,IAAI,SAAS;GACb,IAAI,SAAS;GACb,SAAS,SAAS;EACpB,CAAC;CACH;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta-pixel.mjs","names":[],"sources":["../../src/third-parties/meta-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { type FBQ, type PixelId, mapFBEvent } from '../track/fbq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\n\ndeclare global {\n interface Window {\n /** Undefined until the Meta Pixel script has loaded. */\n fbq?: FBQ['fbq'];\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\nexport function sendFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n event_id?: string\n) {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n if (metrics.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const options = { eventID: event_id };\n const [type, fbEventName, fbEventProperties] = mapFBEvent(name, properties);\n // The two branches are identical on purpose. `fbq` is overloaded per `type` — 'track' takes a\n // standard event name with its typed properties, 'trackCustom' an arbitrary string — and what\n // `mapFBEvent` returns is a union of both shapes. Narrowing `type` is what picks a single\n // overload; collapsing the branches leaves the call matching none of them.\n if (type === 'track') {\n window.fbq(type, fbEventName, fbEventProperties, options);\n } else {\n window.fbq(type, fbEventName, fbEventProperties, options);\n }\n}\n\nexport function setFBUser(pixelId: PixelId) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n\n const address = getFirst(user_data?.address);\n\n window.fbq('init', pixelId, {\n em: getFirst(user_data?.email),\n fn: address?.first_name,\n ln: address?.last_name,\n ph: getFirst(user_data?.phone_number),\n external_id: user_id,\n ct: address?.city,\n st: address?.
|
|
1
|
+
{"version":3,"file":"meta-pixel.mjs","names":[],"sources":["../../src/third-parties/meta-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport { type FBQ, type PixelId, mapFBEvent } from '../track/fbq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\n\ndeclare global {\n interface Window {\n /** Undefined until the Meta Pixel script has loaded. */\n fbq?: FBQ['fbq'];\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\nexport function sendFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n event_id?: string\n) {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n if (metrics.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const options = { eventID: event_id };\n const [type, fbEventName, fbEventProperties] = mapFBEvent(name, properties);\n // The two branches are identical on purpose. `fbq` is overloaded per `type` — 'track' takes a\n // standard event name with its typed properties, 'trackCustom' an arbitrary string — and what\n // `mapFBEvent` returns is a union of both shapes. Narrowing `type` is what picks a single\n // overload; collapsing the branches leaves the call matching none of them.\n if (type === 'track') {\n window.fbq(type, fbEventName, fbEventProperties, options);\n } else {\n window.fbq(type, fbEventName, fbEventProperties, options);\n }\n}\n\nexport function setFBUser(pixelId: PixelId) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.fbq) {\n console.warn('fbq has not been initialized');\n return;\n }\n\n const address = getFirst(user_data?.address);\n\n window.fbq('init', pixelId, {\n em: getFirst(user_data?.email),\n fn: address?.first_name,\n ln: address?.last_name,\n ph: getFirst(user_data?.phone_number),\n external_id: user_id,\n ct: address?.city,\n // `st` is Meta's state/province field, not the street address.\n st: address?.region,\n zp: address?.postal_code,\n country: address?.country,\n });\n };\n}\n"],"mappings":";;;AAYA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;AAE1D,SAAgB,YACd,MACA,YACA,UACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,KAAK;EAChD,QAAQ,KAAK,8BAA8B;EAC3C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,GAAG;CAC5B,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,UAAU,EAAE,SAAS,SAAS;CACpC,MAAM,CAAC,MAAM,aAAa,qBAAqB,WAAW,MAAM,UAAU;CAK1E,IAAI,SAAS,SACX,OAAO,IAAI,MAAM,aAAa,mBAAmB,OAAO;MAExD,OAAO,IAAI,MAAM,aAAa,mBAAmB,OAAO;AAE5D;AAEA,SAAgB,UAAU,SAAkB;CAC1C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,KAAK;GAChD,QAAQ,KAAK,8BAA8B;GAC3C;EACF;EAEA,MAAM,UAAU,SAAS,WAAW,OAAO;EAE3C,OAAO,IAAI,QAAQ,SAAS;GAC1B,IAAI,SAAS,WAAW,KAAK;GAC7B,IAAI,SAAS;GACb,IAAI,SAAS;GACb,IAAI,SAAS,WAAW,YAAY;GACpC,aAAa;GACb,IAAI,SAAS;GAEb,IAAI,SAAS;GACb,IAAI,SAAS;GACb,SAAS,SAAS;EACpB,CAAC;CACH;AACF"}
|
package/dist/track/fbq.cjs
CHANGED
|
@@ -12,9 +12,9 @@ function normalize(parameters) {
|
|
|
12
12
|
zp: parameters.zp?.split("-").at(0)?.trim(),
|
|
13
13
|
fn: parameters.fn?.toLowerCase().trim(),
|
|
14
14
|
ln: parameters.ln?.toLowerCase().trim(),
|
|
15
|
-
ct: parameters.ct?.toLowerCase().replace(/[s/-]/g, "")
|
|
16
|
-
st: parameters.st?.toLowerCase().replace(/[s
|
|
17
|
-
country: parameters.country?.toLowerCase().replace(/[s/-]/g, "")
|
|
15
|
+
ct: parameters.ct?.toLowerCase().replace(/[\s/-]/g, ""),
|
|
16
|
+
st: parameters.st?.toLowerCase().replace(/[\s/,.-]/g, ""),
|
|
17
|
+
country: parameters.country?.toLowerCase().replace(/[\s/-]/g, "")
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
function mapItems(items) {
|
package/dist/track/fbq.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fbq.cjs","names":["p"],"sources":["../../src/track/fbq.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-empty-object-type */\nimport type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport type Content = {\n id: string;\n quantity: number;\n item_price?: number;\n title?: string;\n description?: string;\n brand?: string;\n category?: string;\n delivery_category?: string;\n};\n\n/**\n * reference: https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nexport type MatchingParameters = {\n /** Email: Unhashed lowercase or hashed SHA-256 */\n em?: string;\n\n /** First Name: Lowercase letters */\n fn?: string;\n\n /** Last Name: Lowercase letters */\n ln?: string;\n\n /** Phone Number: Digits only including country code and area code */\n ph?: string;\n\n /**\n * External ID: Any unique ID from the advertiser, such as loyalty membership ID, user ID, and\n * external cookie ID.\n */\n external_id?: string;\n\n /** Gender: Single lowercase letter, f or m, if unknown, leave blank */\n ge?: 'f' | 'm' | '';\n\n /** Birthdate: Digits only with birth year, month, then day, YYYYMMDD */\n db?: number;\n\n /** City: Lowercase with any spaces removed, e.g. \"menlopark\" */\n ct?: string;\n\n /** State or Province: Lowercase two-letter state or province code, e.g. \"ca\" */\n st?: string;\n\n /** Zip or Postal Code: String */\n zp?: string;\n\n /** Country: Lowercase two-letter country code, e.g. \"us\" */\n country?: string;\n\n /** Client IP Address: Do not hash. */\n client_ip_address?: string;\n\n /** Client User Agent: Do not hash. */\n client_user_agent?: string;\n\n /**\n * Click ID: Do not hash.\n * The Facebook click ID value is stored in the _fbc browser cookie under your domain. See\n * Managing fbc and fbp Parameters for how to get this value or generate this value from a fbclid\n * query parameter.\n *\n * The format is fb.${subdomain_index}.${creation_time}.${fbclid}.\n */\n fbc?: string;\n\n /**\n * Browser ID: Do not hash.\n * The Facebook browser ID value is stored in the _fbp browser cookie under your domain. See\n * Managing fbc and fbp Parameters for how to get this value.\n *\n * The format is fb.${subdomain_index}.${creation_time}.${random_number}.\n */\n fbp?: string;\n\n /**\n * Subscription ID: Do not hash.\n * The subscription ID for the user in this transaction; it is similar to the order ID for an\n * individual product.\n */\n subscription_id?: string;\n\n /**\n * Facebook Login ID: Do not hash.\n * The ID issued by Meta when a person first logs into an instance of an app. This is also known\n * as App-Scoped ID.\n */\n fb_login_id?: number;\n\n /**\n * Lead ID: Do not hash.\n * The ID associated with a lead generated by [Meta's Lead Ads](https://developers.facebook.com/docs/marketing-api/guides/lead-ads).\n */\n lead_id?: number;\n\n /**\n * Install ID: Do not hash.\n * Your install ID. This field represents unique application installation instances.\n * Note: This parameter is for app events only.\n */\n anon_id?: string;\n\n /**\n * Your mobile advertiser ID, the advertising ID from an Android device or the Advertising\n * Identifier (IDFA) from an Apple device.\n */\n madid?: string;\n\n /**\n * Page ID: Do not hash.\n * Your Page ID. Specifies the page ID associated with the event. Use the Facebook page ID of the\n * page associated with the bot.\n */\n page_id?: string;\n\n /**\n * Page Scoped User ID: Do not hash.\n * Specifies the page-scoped user ID associated with the messenger bot that logs the event. Use\n * the page-scoped user ID provided to your webhook.\n */\n page_scoped_user_id?: string;\n\n /**\n * Do not hash.\n * Click ID generated by Meta for ads that click to WhatsApp.\n */\n ctwa_clid?: string;\n\n /**\n * Do not hash.\n * Instagram Account ID that is associated with the business.\n */\n ig_account_id?: string;\n\n /**\n * Do not hash.\n * Users who interact with Instagram are identified by Instagram-Scoped User IDs (IGSID). IGSID\n * can be obtained from this webhook.\n */\n ig_sid?: string;\n};\n\n/**\n * You can include the following predefined object properties with any custom events, and any\n * standard events that support them. Format your parameter object data using JSON. Learn more about\n * event parameters with Blueprint.\n */\nexport type ObjectProperties = {\n content_category?: string;\n content_ids?: string[];\n content_name?: string;\n\n /**\n * Either product or product_group based on the content_ids or contents being passed. If the IDs\n * being passed in content_ids or contents parameter are IDs of products, then the value should be\n * product. If product group IDs are being passed, then the value should be product_group.\n *\n * If no content_type is provided, Meta will match the event to every item that has the same ID,\n * independent of its type.\n */\n content_type?: 'product' | 'product_group' | (string & {});\n contents?: Content[];\n delivery_category?: 'in_store' | 'curbside' | 'home_delivery';\n currency?: string;\n num_items?: number;\n predicted_ltv?: number;\n search_string?: string;\n\n /** Used with the CompleteRegistration event, to show the status of the registration. */\n status?: boolean;\n value?: number;\n};\n\n/**\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/payload-helper\n */\nexport type StandardEvents = {\n AddPaymentInfo: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n value?: number;\n };\n AddToCart: {\n content_ids?: string[];\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents\n currency?: string;\n value?: number;\n };\n AddToWishlist: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n value?: number;\n };\n CompleteRegistration: {\n currency?: string;\n value?: number;\n method?: string;\n };\n Contact: {};\n CustomizeProduct: {};\n Donate: {};\n FindLocation: {};\n InitiateCheckout: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n num_items?: number;\n value?: number;\n };\n Lead: {\n currency?: string;\n value?: number;\n };\n Purchase: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency: string; // required\n num_items?: number;\n value: number; // required\n };\n Schedule: {};\n Search: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency?: string;\n search_string?: string;\n value?: number;\n };\n StartTrial: {\n currency?: string;\n predicted_ltv?: number;\n value?: number;\n };\n SubmitApplication: {};\n Subscribe: {\n currency?: string;\n predicted_ltv?: number;\n value?: number;\n };\n ViewContent: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency?: string;\n value?: number;\n };\n};\n\ntype JSONValue =\n | null\n | string\n | number\n | boolean\n | Array<JSONValue>\n | { [value: string]: JSONValue };\n\nexport type PixelId = `${number}`;\nexport type Options = { eventID?: string };\n\n/**\n * reference: https://developers.facebook.com/docs/meta-pixel/reference#standard-events\n *\n * We determine if events are identical based on their ID and name. So, for an event to be deduplicated:\n * - In corresponding events, a Meta Pixel's eventID must match the Conversion API's event_id.\n * - In corresponding events, a Meta Pixel's event must match the Conversion API's event_name.\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nexport interface FBQ {\n /**\n * reference: https://stackoverflow.com/questions/62304291/sending-user-data-parameters-via-pixel\n *\n * Call init the normal default way first:\n * `fbq('init', 'XXXXX')`\n *\n * And at a later point in time, when you have obtained additional user data, you can call init\n * again basically enriching the already running fbq instance with additional data:\n * `fbq('init', 'XXXXX', { external_id: 1234, em: 'abc@abc.com' } )`\n *\n * Only caveat is that you have to send an event after this additional init call, otherwise the\n * provided data will not be sent to Facebook.\n */\n fbq(type: 'init', pixelId: PixelId, parameters?: MatchingParameters): void;\n\n /** Enable Manual Only mode. (value = false) */\n fbq(type: 'set', key: 'autoConfig', value: boolean, pixelId: PixelId): void;\n\n fbq<T extends keyof StandardEvents>(\n type: 'track',\n event: T,\n properties?: StandardEvents[T] & ObjectProperties,\n options?: Options\n ): void;\n\n fbq(\n type: 'trackCustom',\n event: string,\n properties?: Record<string, JSONValue> & ObjectProperties,\n options?: Options\n ): void;\n\n /** https://developers.facebook.com/docs/meta-pixel/guides/track-multiple-events/ */\n fbq<T extends keyof StandardEvents>(\n type: 'trackSingle',\n pixelId: PixelId,\n event: T,\n properties?: StandardEvents[T] & ObjectProperties,\n options?: Options\n ): void;\n\n /** https://developers.facebook.com/docs/meta-pixel/guides/track-multiple-events/ */\n fbq(\n type: 'trackSingleCustom',\n pixelId: PixelId,\n event: string,\n properties?: Record<string, JSONValue> & ObjectProperties,\n options?: Options\n ): void;\n}\n\n/**\n * Please download this CSV filefor examples of properly normalized and hashed data for the\n * parameters below.\n */\nexport function normalize(parameters: MatchingParameters): MatchingParameters {\n return {\n ...parameters,\n em: parameters.em?.toLowerCase().trim(),\n ph: parameters.ph?.replace(/[-+()\\s]/g, '').replace(/^0+/, ''),\n zp: parameters.zp?.split('-').at(0)?.trim(),\n fn: parameters.fn?.toLowerCase().trim(),\n ln: parameters.ln?.toLowerCase().trim(),\n ct: parameters.ct?.toLowerCase().replace(/[s/-]/g, '').trim(),\n st: parameters.st\n ?.toLowerCase()\n .replace(/[s/-/,.]/g, '')\n .trim(),\n country: parameters.country?.toLowerCase().replace(/[s/-]/g, '').trim(),\n };\n}\n\nexport function mapItems(items?: Item[]): ObjectProperties {\n if (!items) return {};\n const categories = Array.from(new Set(items.map((i) => i.item_category).filter(Boolean)));\n const contents: Content[] = items.map(\n ({ item_id, quantity, price, item_name, item_brand, item_category, ..._others }) => ({\n id: item_id,\n quantity: quantity ?? 1,\n item_price: price,\n title: item_name,\n brand: item_brand,\n category: item_category,\n })\n );\n\n return {\n content_category: categories.length === 1 ? categories.at(0) : undefined,\n contents,\n content_ids: contents.map((c) => c.id),\n num_items: items.reduce((acc, i) => acc + (i.quantity ?? 1), 0),\n };\n}\n\ntype Mapped<F extends keyof StandardEvents> = ['track', F, StandardEvents[F] & ObjectProperties];\ntype Missed<F extends string> = ['trackCustom', F, Record<string, JSONValue> & ObjectProperties];\n\nexport function mapFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): Mapped<keyof StandardEvents> | Missed<TrackName<T>> {\n if (name === 'add_payment_info') {\n const p = properties as TrackProperties<'add_payment_info'> | undefined;\n return [\n 'track',\n 'AddPaymentInfo',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'add_to_cart') {\n const p = properties as TrackProperties<'add_to_cart'> | undefined;\n return [\n 'track',\n 'AddToCart',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'add_to_wishlist') {\n const p = properties as TrackProperties<'add_to_wishlist'> | undefined;\n return [\n 'track',\n 'AddToWishlist',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'login') {\n const p = properties as TrackProperties<'login'> | undefined;\n return ['track', 'CompleteRegistration', { method: p?.method }];\n } else if (name === 'contact') {\n return ['track', 'Contact', {}];\n } else if (name === 'customize_product') {\n return ['track', 'CustomizeProduct', {}];\n } else if (name === 'donate') {\n return ['track', 'Donate', {}];\n } else if (name === 'find_location') {\n return ['track', 'FindLocation', {}];\n } else if (name === 'begin_checkout') {\n const p = properties as TrackProperties<'begin_checkout'> | undefined;\n return [\n 'track',\n 'InitiateCheckout',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'generate_lead') {\n const p = properties as TrackProperties<'generate_lead'> | undefined;\n return ['track', 'Lead', { currency: p?.currency, value: p?.value }];\n } else if (name === 'purchase') {\n const p = properties as TrackProperties<'purchase'> | undefined;\n return [\n 'track',\n 'Purchase',\n { currency: p?.currency ?? 'USD', value: p?.value ?? 0, ...mapItems(p?.items) },\n ];\n } else if (name === 'schedule') {\n return ['track', 'Schedule', {}];\n } else if (name === 'search') {\n const p = properties as TrackProperties<'search'> | undefined;\n return ['track', 'Search', { search_string: p?.search_term }];\n } else if (name === 'trial_begin') {\n const p = properties as TrackProperties<'trial_begin'> | undefined;\n return ['track', 'StartTrial', { currency: p?.currency, value: p?.value }];\n } else if (name === 'submit_application') {\n return ['track', 'SubmitApplication', {}];\n } else if (name === 'subscribe') {\n const p = properties as TrackProperties<'subscribe'> | undefined;\n return ['track', 'Subscribe', { currency: p?.currency, value: p?.value }];\n } else if (name === 'view_item') {\n const p = properties as TrackProperties<'view_item'> | undefined;\n return [\n 'track',\n 'ViewContent',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else {\n return ['trackCustom', name, properties ?? {}];\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fbq.cjs","names":["p"],"sources":["../../src/track/fbq.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-empty-object-type */\nimport type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport type Content = {\n id: string;\n quantity: number;\n item_price?: number;\n title?: string;\n description?: string;\n brand?: string;\n category?: string;\n delivery_category?: string;\n};\n\n/**\n * reference: https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nexport type MatchingParameters = {\n /** Email: Unhashed lowercase or hashed SHA-256 */\n em?: string;\n\n /** First Name: Lowercase letters */\n fn?: string;\n\n /** Last Name: Lowercase letters */\n ln?: string;\n\n /** Phone Number: Digits only including country code and area code */\n ph?: string;\n\n /**\n * External ID: Any unique ID from the advertiser, such as loyalty membership ID, user ID, and\n * external cookie ID.\n */\n external_id?: string;\n\n /** Gender: Single lowercase letter, f or m, if unknown, leave blank */\n ge?: 'f' | 'm' | '';\n\n /** Birthdate: Digits only with birth year, month, then day, YYYYMMDD */\n db?: number;\n\n /** City: Lowercase with any spaces removed, e.g. \"menlopark\" */\n ct?: string;\n\n /** State or Province: Lowercase two-letter state or province code, e.g. \"ca\" */\n st?: string;\n\n /** Zip or Postal Code: String */\n zp?: string;\n\n /** Country: Lowercase two-letter country code, e.g. \"us\" */\n country?: string;\n\n /** Client IP Address: Do not hash. */\n client_ip_address?: string;\n\n /** Client User Agent: Do not hash. */\n client_user_agent?: string;\n\n /**\n * Click ID: Do not hash.\n * The Facebook click ID value is stored in the _fbc browser cookie under your domain. See\n * Managing fbc and fbp Parameters for how to get this value or generate this value from a fbclid\n * query parameter.\n *\n * The format is fb.${subdomain_index}.${creation_time}.${fbclid}.\n */\n fbc?: string;\n\n /**\n * Browser ID: Do not hash.\n * The Facebook browser ID value is stored in the _fbp browser cookie under your domain. See\n * Managing fbc and fbp Parameters for how to get this value.\n *\n * The format is fb.${subdomain_index}.${creation_time}.${random_number}.\n */\n fbp?: string;\n\n /**\n * Subscription ID: Do not hash.\n * The subscription ID for the user in this transaction; it is similar to the order ID for an\n * individual product.\n */\n subscription_id?: string;\n\n /**\n * Facebook Login ID: Do not hash.\n * The ID issued by Meta when a person first logs into an instance of an app. This is also known\n * as App-Scoped ID.\n */\n fb_login_id?: number;\n\n /**\n * Lead ID: Do not hash.\n * The ID associated with a lead generated by [Meta's Lead Ads](https://developers.facebook.com/docs/marketing-api/guides/lead-ads).\n */\n lead_id?: number;\n\n /**\n * Install ID: Do not hash.\n * Your install ID. This field represents unique application installation instances.\n * Note: This parameter is for app events only.\n */\n anon_id?: string;\n\n /**\n * Your mobile advertiser ID, the advertising ID from an Android device or the Advertising\n * Identifier (IDFA) from an Apple device.\n */\n madid?: string;\n\n /**\n * Page ID: Do not hash.\n * Your Page ID. Specifies the page ID associated with the event. Use the Facebook page ID of the\n * page associated with the bot.\n */\n page_id?: string;\n\n /**\n * Page Scoped User ID: Do not hash.\n * Specifies the page-scoped user ID associated with the messenger bot that logs the event. Use\n * the page-scoped user ID provided to your webhook.\n */\n page_scoped_user_id?: string;\n\n /**\n * Do not hash.\n * Click ID generated by Meta for ads that click to WhatsApp.\n */\n ctwa_clid?: string;\n\n /**\n * Do not hash.\n * Instagram Account ID that is associated with the business.\n */\n ig_account_id?: string;\n\n /**\n * Do not hash.\n * Users who interact with Instagram are identified by Instagram-Scoped User IDs (IGSID). IGSID\n * can be obtained from this webhook.\n */\n ig_sid?: string;\n};\n\n/**\n * You can include the following predefined object properties with any custom events, and any\n * standard events that support them. Format your parameter object data using JSON. Learn more about\n * event parameters with Blueprint.\n */\nexport type ObjectProperties = {\n content_category?: string;\n content_ids?: string[];\n content_name?: string;\n\n /**\n * Either product or product_group based on the content_ids or contents being passed. If the IDs\n * being passed in content_ids or contents parameter are IDs of products, then the value should be\n * product. If product group IDs are being passed, then the value should be product_group.\n *\n * If no content_type is provided, Meta will match the event to every item that has the same ID,\n * independent of its type.\n */\n content_type?: 'product' | 'product_group' | (string & {});\n contents?: Content[];\n delivery_category?: 'in_store' | 'curbside' | 'home_delivery';\n currency?: string;\n num_items?: number;\n predicted_ltv?: number;\n search_string?: string;\n\n /** Used with the CompleteRegistration event, to show the status of the registration. */\n status?: boolean;\n value?: number;\n};\n\n/**\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/payload-helper\n */\nexport type StandardEvents = {\n AddPaymentInfo: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n value?: number;\n };\n AddToCart: {\n content_ids?: string[];\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents\n currency?: string;\n value?: number;\n };\n AddToWishlist: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n value?: number;\n };\n CompleteRegistration: {\n currency?: string;\n value?: number;\n method?: string;\n };\n Contact: {};\n CustomizeProduct: {};\n Donate: {};\n FindLocation: {};\n InitiateCheckout: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n num_items?: number;\n value?: number;\n };\n Lead: {\n currency?: string;\n value?: number;\n };\n Purchase: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency: string; // required\n num_items?: number;\n value: number; // required\n };\n Schedule: {};\n Search: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency?: string;\n search_string?: string;\n value?: number;\n };\n StartTrial: {\n currency?: string;\n predicted_ltv?: number;\n value?: number;\n };\n SubmitApplication: {};\n Subscribe: {\n currency?: string;\n predicted_ltv?: number;\n value?: number;\n };\n ViewContent: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency?: string;\n value?: number;\n };\n};\n\ntype JSONValue =\n | null\n | string\n | number\n | boolean\n | Array<JSONValue>\n | { [value: string]: JSONValue };\n\nexport type PixelId = `${number}`;\nexport type Options = { eventID?: string };\n\n/**\n * reference: https://developers.facebook.com/docs/meta-pixel/reference#standard-events\n *\n * We determine if events are identical based on their ID and name. So, for an event to be deduplicated:\n * - In corresponding events, a Meta Pixel's eventID must match the Conversion API's event_id.\n * - In corresponding events, a Meta Pixel's event must match the Conversion API's event_name.\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nexport interface FBQ {\n /**\n * reference: https://stackoverflow.com/questions/62304291/sending-user-data-parameters-via-pixel\n *\n * Call init the normal default way first:\n * `fbq('init', 'XXXXX')`\n *\n * And at a later point in time, when you have obtained additional user data, you can call init\n * again basically enriching the already running fbq instance with additional data:\n * `fbq('init', 'XXXXX', { external_id: 1234, em: 'abc@abc.com' } )`\n *\n * Only caveat is that you have to send an event after this additional init call, otherwise the\n * provided data will not be sent to Facebook.\n */\n fbq(type: 'init', pixelId: PixelId, parameters?: MatchingParameters): void;\n\n /** Enable Manual Only mode. (value = false) */\n fbq(type: 'set', key: 'autoConfig', value: boolean, pixelId: PixelId): void;\n\n fbq<T extends keyof StandardEvents>(\n type: 'track',\n event: T,\n properties?: StandardEvents[T] & ObjectProperties,\n options?: Options\n ): void;\n\n fbq(\n type: 'trackCustom',\n event: string,\n properties?: Record<string, JSONValue> & ObjectProperties,\n options?: Options\n ): void;\n\n /** https://developers.facebook.com/docs/meta-pixel/guides/track-multiple-events/ */\n fbq<T extends keyof StandardEvents>(\n type: 'trackSingle',\n pixelId: PixelId,\n event: T,\n properties?: StandardEvents[T] & ObjectProperties,\n options?: Options\n ): void;\n\n /** https://developers.facebook.com/docs/meta-pixel/guides/track-multiple-events/ */\n fbq(\n type: 'trackSingleCustom',\n pixelId: PixelId,\n event: string,\n properties?: Record<string, JSONValue> & ObjectProperties,\n options?: Options\n ): void;\n}\n\n/**\n * Please download this CSV filefor examples of properly normalized and hashed data for the\n * parameters below.\n */\nexport function normalize(parameters: MatchingParameters): MatchingParameters {\n return {\n ...parameters,\n em: parameters.em?.toLowerCase().trim(),\n ph: parameters.ph?.replace(/[-+()\\s]/g, '').replace(/^0+/, ''),\n zp: parameters.zp?.split('-').at(0)?.trim(),\n fn: parameters.fn?.toLowerCase().trim(),\n ln: parameters.ln?.toLowerCase().trim(),\n ct: parameters.ct?.toLowerCase().replace(/[\\s/-]/g, ''),\n st: parameters.st?.toLowerCase().replace(/[\\s/,.-]/g, ''),\n country: parameters.country?.toLowerCase().replace(/[\\s/-]/g, ''),\n };\n}\n\nexport function mapItems(items?: Item[]): ObjectProperties {\n if (!items) return {};\n const categories = Array.from(new Set(items.map((i) => i.item_category).filter(Boolean)));\n const contents: Content[] = items.map(\n ({ item_id, quantity, price, item_name, item_brand, item_category, ..._others }) => ({\n id: item_id,\n quantity: quantity ?? 1,\n item_price: price,\n title: item_name,\n brand: item_brand,\n category: item_category,\n })\n );\n\n return {\n content_category: categories.length === 1 ? categories.at(0) : undefined,\n contents,\n content_ids: contents.map((c) => c.id),\n num_items: items.reduce((acc, i) => acc + (i.quantity ?? 1), 0),\n };\n}\n\ntype Mapped<F extends keyof StandardEvents> = ['track', F, StandardEvents[F] & ObjectProperties];\ntype Missed<F extends string> = ['trackCustom', F, Record<string, JSONValue> & ObjectProperties];\n\nexport function mapFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): Mapped<keyof StandardEvents> | Missed<TrackName<T>> {\n if (name === 'add_payment_info') {\n const p = properties as TrackProperties<'add_payment_info'> | undefined;\n return [\n 'track',\n 'AddPaymentInfo',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'add_to_cart') {\n const p = properties as TrackProperties<'add_to_cart'> | undefined;\n return [\n 'track',\n 'AddToCart',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'add_to_wishlist') {\n const p = properties as TrackProperties<'add_to_wishlist'> | undefined;\n return [\n 'track',\n 'AddToWishlist',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'login') {\n const p = properties as TrackProperties<'login'> | undefined;\n return ['track', 'CompleteRegistration', { method: p?.method }];\n } else if (name === 'contact') {\n return ['track', 'Contact', {}];\n } else if (name === 'customize_product') {\n return ['track', 'CustomizeProduct', {}];\n } else if (name === 'donate') {\n return ['track', 'Donate', {}];\n } else if (name === 'find_location') {\n return ['track', 'FindLocation', {}];\n } else if (name === 'begin_checkout') {\n const p = properties as TrackProperties<'begin_checkout'> | undefined;\n return [\n 'track',\n 'InitiateCheckout',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'generate_lead') {\n const p = properties as TrackProperties<'generate_lead'> | undefined;\n return ['track', 'Lead', { currency: p?.currency, value: p?.value }];\n } else if (name === 'purchase') {\n const p = properties as TrackProperties<'purchase'> | undefined;\n return [\n 'track',\n 'Purchase',\n { currency: p?.currency ?? 'USD', value: p?.value ?? 0, ...mapItems(p?.items) },\n ];\n } else if (name === 'schedule') {\n return ['track', 'Schedule', {}];\n } else if (name === 'search') {\n const p = properties as TrackProperties<'search'> | undefined;\n return ['track', 'Search', { search_string: p?.search_term }];\n } else if (name === 'trial_begin') {\n const p = properties as TrackProperties<'trial_begin'> | undefined;\n return ['track', 'StartTrial', { currency: p?.currency, value: p?.value }];\n } else if (name === 'submit_application') {\n return ['track', 'SubmitApplication', {}];\n } else if (name === 'subscribe') {\n const p = properties as TrackProperties<'subscribe'> | undefined;\n return ['track', 'Subscribe', { currency: p?.currency, value: p?.value }];\n } else if (name === 'view_item') {\n const p = properties as TrackProperties<'view_item'> | undefined;\n return [\n 'track',\n 'ViewContent',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else {\n return ['trackCustom', name, properties ?? {}];\n }\n}\n"],"mappings":"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"}
|
package/dist/track/fbq.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fbq.d.cts","names":[],"sources":["../../src/track/fbq.ts"],"mappings":";;;KAIY;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;KAOU;;EAEV;;EAGA;;EAGA;;EAGA;;;;;EAMA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;;;;;;;EAUA;;;;;;;;EASA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;;EAOA;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;;KAQU;EACV;EACA;EACA;;;;;;;;;EAUA;EACA,WAAW;EACX;EACA;EACA;EACA;EACA;;EAGA;EACA;;;;;KAMU;EACV;IACE;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA;IACA;;EAEF;EACA;EACA;EACA;EACA;IACE;IACA,WAAW;IACX;IACA;IACA;;EAEF;IACE;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;IACA;;EAEF;EACA;IACE;IACA;IACA,WAAW;IACX;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;EACA;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;;;KAIC,+CAKD,MAAM;GACH,gBAAgB;;KAEX;KACA;EAAY;;;;;;;;;;;UAWP;;;;;;;;;;;;;;EAcf,IAAI,cAAc,SAAS,SAAS,aAAa;;EAGjD,IAAI,aAAa,mBAAmB,gBAAgB,SAAS;EAE7D,IAAI,gBAAgB,gBAClB,eACA,OAAO,GACP,aAAa,eAAe,KAAK,kBACjC,UAAU;EAGZ,IACE,qBACA,eACA,aAAa,eAAe,aAAa,kBACzC,UAAU;;EAIZ,IAAI,gBAAgB,gBAClB,qBACA,SAAS,SACT,OAAO,GACP,aAAa,eAAe,KAAK,kBACjC,UAAU;;EAIZ,IACE,2BACA,SAAS,SACT,eACA,aAAa,eAAe,aAAa,kBACzC,UAAU;;;;;;iBAQE,UAAU,YAAY,qBAAqB;
|
|
1
|
+
{"version":3,"file":"fbq.d.cts","names":[],"sources":["../../src/track/fbq.ts"],"mappings":";;;KAIY;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;KAOU;;EAEV;;EAGA;;EAGA;;EAGA;;;;;EAMA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;;;;;;;EAUA;;;;;;;;EASA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;;EAOA;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;;KAQU;EACV;EACA;EACA;;;;;;;;;EAUA;EACA,WAAW;EACX;EACA;EACA;EACA;EACA;;EAGA;EACA;;;;;KAMU;EACV;IACE;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA;IACA;;EAEF;EACA;EACA;EACA;EACA;IACE;IACA,WAAW;IACX;IACA;IACA;;EAEF;IACE;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;IACA;;EAEF;EACA;IACE;IACA;IACA,WAAW;IACX;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;EACA;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;;;KAIC,+CAKD,MAAM;GACH,gBAAgB;;KAEX;KACA;EAAY;;;;;;;;;;;UAWP;;;;;;;;;;;;;;EAcf,IAAI,cAAc,SAAS,SAAS,aAAa;;EAGjD,IAAI,aAAa,mBAAmB,gBAAgB,SAAS;EAE7D,IAAI,gBAAgB,gBAClB,eACA,OAAO,GACP,aAAa,eAAe,KAAK,kBACjC,UAAU;EAGZ,IACE,qBACA,eACA,aAAa,eAAe,aAAa,kBACzC,UAAU;;EAIZ,IAAI,gBAAgB,gBAClB,qBACA,SAAS,SACT,OAAO,GACP,aAAa,eAAe,KAAK,kBACjC,UAAU;;EAIZ,IACE,2BACA,SAAS,SACT,eACA,aAAa,eAAe,aAAa,kBACzC,UAAU;;;;;;iBAQE,UAAU,YAAY,qBAAqB;iBAc3C,SAAS,QAAQ,SAAS;KAsBrC,OAAO,gBAAgB,4BAA4B,GAAG,eAAe,KAAK;KAC1E,OAAO,oCAAoC,GAAG,eAAe,aAAa;iBAE/D,WAAW,UAAU,WACnC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B,aAAa,kBAAkB,OAAO,UAAU"}
|
package/dist/track/fbq.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fbq.d.mts","names":[],"sources":["../../src/track/fbq.ts"],"mappings":";;;KAIY;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;KAOU;;EAEV;;EAGA;;EAGA;;EAGA;;;;;EAMA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;;;;;;;EAUA;;;;;;;;EASA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;;EAOA;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;;KAQU;EACV;EACA;EACA;;;;;;;;;EAUA;EACA,WAAW;EACX;EACA;EACA;EACA;EACA;;EAGA;EACA;;;;;KAMU;EACV;IACE;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA;IACA;;EAEF;EACA;EACA;EACA;EACA;IACE;IACA,WAAW;IACX;IACA;IACA;;EAEF;IACE;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;IACA;;EAEF;EACA;IACE;IACA;IACA,WAAW;IACX;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;EACA;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;;;KAIC,+CAKD,MAAM;GACH,gBAAgB;;KAEX;KACA;EAAY;;;;;;;;;;;UAWP;;;;;;;;;;;;;;EAcf,IAAI,cAAc,SAAS,SAAS,aAAa;;EAGjD,IAAI,aAAa,mBAAmB,gBAAgB,SAAS;EAE7D,IAAI,gBAAgB,gBAClB,eACA,OAAO,GACP,aAAa,eAAe,KAAK,kBACjC,UAAU;EAGZ,IACE,qBACA,eACA,aAAa,eAAe,aAAa,kBACzC,UAAU;;EAIZ,IAAI,gBAAgB,gBAClB,qBACA,SAAS,SACT,OAAO,GACP,aAAa,eAAe,KAAK,kBACjC,UAAU;;EAIZ,IACE,2BACA,SAAS,SACT,eACA,aAAa,eAAe,aAAa,kBACzC,UAAU;;;;;;iBAQE,UAAU,YAAY,qBAAqB;
|
|
1
|
+
{"version":3,"file":"fbq.d.mts","names":[],"sources":["../../src/track/fbq.ts"],"mappings":";;;KAIY;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;KAOU;;EAEV;;EAGA;;EAGA;;EAGA;;;;;EAMA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;;;;;;;EAUA;;;;;;;;EASA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;;EAOA;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;;KAQU;EACV;EACA;EACA;;;;;;;;;EAUA;EACA,WAAW;EACX;EACA;EACA;EACA;EACA;;EAGA;EACA;;;;;KAMU;EACV;IACE;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA,WAAW;IACX;IACA;;EAEF;IACE;IACA;IACA;;EAEF;EACA;EACA;EACA;EACA;IACE;IACA,WAAW;IACX;IACA;IACA;;EAEF;IACE;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;IACA;;EAEF;EACA;IACE;IACA;IACA,WAAW;IACX;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;EACA;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA,WAAW;IACX;IACA;;;KAIC,+CAKD,MAAM;GACH,gBAAgB;;KAEX;KACA;EAAY;;;;;;;;;;;UAWP;;;;;;;;;;;;;;EAcf,IAAI,cAAc,SAAS,SAAS,aAAa;;EAGjD,IAAI,aAAa,mBAAmB,gBAAgB,SAAS;EAE7D,IAAI,gBAAgB,gBAClB,eACA,OAAO,GACP,aAAa,eAAe,KAAK,kBACjC,UAAU;EAGZ,IACE,qBACA,eACA,aAAa,eAAe,aAAa,kBACzC,UAAU;;EAIZ,IAAI,gBAAgB,gBAClB,qBACA,SAAS,SACT,OAAO,GACP,aAAa,eAAe,KAAK,kBACjC,UAAU;;EAIZ,IACE,2BACA,SAAS,SACT,eACA,aAAa,eAAe,aAAa,kBACzC,UAAU;;;;;;iBAQE,UAAU,YAAY,qBAAqB;iBAc3C,SAAS,QAAQ,SAAS;KAsBrC,OAAO,gBAAgB,4BAA4B,GAAG,eAAe,KAAK;KAC1E,OAAO,oCAAoC,GAAG,eAAe,aAAa;iBAE/D,WAAW,UAAU,WACnC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B,aAAa,kBAAkB,OAAO,UAAU"}
|
package/dist/track/fbq.mjs
CHANGED
|
@@ -11,9 +11,9 @@ function normalize(parameters) {
|
|
|
11
11
|
zp: parameters.zp?.split("-").at(0)?.trim(),
|
|
12
12
|
fn: parameters.fn?.toLowerCase().trim(),
|
|
13
13
|
ln: parameters.ln?.toLowerCase().trim(),
|
|
14
|
-
ct: parameters.ct?.toLowerCase().replace(/[s/-]/g, "")
|
|
15
|
-
st: parameters.st?.toLowerCase().replace(/[s
|
|
16
|
-
country: parameters.country?.toLowerCase().replace(/[s/-]/g, "")
|
|
14
|
+
ct: parameters.ct?.toLowerCase().replace(/[\s/-]/g, ""),
|
|
15
|
+
st: parameters.st?.toLowerCase().replace(/[\s/,.-]/g, ""),
|
|
16
|
+
country: parameters.country?.toLowerCase().replace(/[\s/-]/g, "")
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
function mapItems(items) {
|
package/dist/track/fbq.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fbq.mjs","names":["p"],"sources":["../../src/track/fbq.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-empty-object-type */\nimport type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport type Content = {\n id: string;\n quantity: number;\n item_price?: number;\n title?: string;\n description?: string;\n brand?: string;\n category?: string;\n delivery_category?: string;\n};\n\n/**\n * reference: https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nexport type MatchingParameters = {\n /** Email: Unhashed lowercase or hashed SHA-256 */\n em?: string;\n\n /** First Name: Lowercase letters */\n fn?: string;\n\n /** Last Name: Lowercase letters */\n ln?: string;\n\n /** Phone Number: Digits only including country code and area code */\n ph?: string;\n\n /**\n * External ID: Any unique ID from the advertiser, such as loyalty membership ID, user ID, and\n * external cookie ID.\n */\n external_id?: string;\n\n /** Gender: Single lowercase letter, f or m, if unknown, leave blank */\n ge?: 'f' | 'm' | '';\n\n /** Birthdate: Digits only with birth year, month, then day, YYYYMMDD */\n db?: number;\n\n /** City: Lowercase with any spaces removed, e.g. \"menlopark\" */\n ct?: string;\n\n /** State or Province: Lowercase two-letter state or province code, e.g. \"ca\" */\n st?: string;\n\n /** Zip or Postal Code: String */\n zp?: string;\n\n /** Country: Lowercase two-letter country code, e.g. \"us\" */\n country?: string;\n\n /** Client IP Address: Do not hash. */\n client_ip_address?: string;\n\n /** Client User Agent: Do not hash. */\n client_user_agent?: string;\n\n /**\n * Click ID: Do not hash.\n * The Facebook click ID value is stored in the _fbc browser cookie under your domain. See\n * Managing fbc and fbp Parameters for how to get this value or generate this value from a fbclid\n * query parameter.\n *\n * The format is fb.${subdomain_index}.${creation_time}.${fbclid}.\n */\n fbc?: string;\n\n /**\n * Browser ID: Do not hash.\n * The Facebook browser ID value is stored in the _fbp browser cookie under your domain. See\n * Managing fbc and fbp Parameters for how to get this value.\n *\n * The format is fb.${subdomain_index}.${creation_time}.${random_number}.\n */\n fbp?: string;\n\n /**\n * Subscription ID: Do not hash.\n * The subscription ID for the user in this transaction; it is similar to the order ID for an\n * individual product.\n */\n subscription_id?: string;\n\n /**\n * Facebook Login ID: Do not hash.\n * The ID issued by Meta when a person first logs into an instance of an app. This is also known\n * as App-Scoped ID.\n */\n fb_login_id?: number;\n\n /**\n * Lead ID: Do not hash.\n * The ID associated with a lead generated by [Meta's Lead Ads](https://developers.facebook.com/docs/marketing-api/guides/lead-ads).\n */\n lead_id?: number;\n\n /**\n * Install ID: Do not hash.\n * Your install ID. This field represents unique application installation instances.\n * Note: This parameter is for app events only.\n */\n anon_id?: string;\n\n /**\n * Your mobile advertiser ID, the advertising ID from an Android device or the Advertising\n * Identifier (IDFA) from an Apple device.\n */\n madid?: string;\n\n /**\n * Page ID: Do not hash.\n * Your Page ID. Specifies the page ID associated with the event. Use the Facebook page ID of the\n * page associated with the bot.\n */\n page_id?: string;\n\n /**\n * Page Scoped User ID: Do not hash.\n * Specifies the page-scoped user ID associated with the messenger bot that logs the event. Use\n * the page-scoped user ID provided to your webhook.\n */\n page_scoped_user_id?: string;\n\n /**\n * Do not hash.\n * Click ID generated by Meta for ads that click to WhatsApp.\n */\n ctwa_clid?: string;\n\n /**\n * Do not hash.\n * Instagram Account ID that is associated with the business.\n */\n ig_account_id?: string;\n\n /**\n * Do not hash.\n * Users who interact with Instagram are identified by Instagram-Scoped User IDs (IGSID). IGSID\n * can be obtained from this webhook.\n */\n ig_sid?: string;\n};\n\n/**\n * You can include the following predefined object properties with any custom events, and any\n * standard events that support them. Format your parameter object data using JSON. Learn more about\n * event parameters with Blueprint.\n */\nexport type ObjectProperties = {\n content_category?: string;\n content_ids?: string[];\n content_name?: string;\n\n /**\n * Either product or product_group based on the content_ids or contents being passed. If the IDs\n * being passed in content_ids or contents parameter are IDs of products, then the value should be\n * product. If product group IDs are being passed, then the value should be product_group.\n *\n * If no content_type is provided, Meta will match the event to every item that has the same ID,\n * independent of its type.\n */\n content_type?: 'product' | 'product_group' | (string & {});\n contents?: Content[];\n delivery_category?: 'in_store' | 'curbside' | 'home_delivery';\n currency?: string;\n num_items?: number;\n predicted_ltv?: number;\n search_string?: string;\n\n /** Used with the CompleteRegistration event, to show the status of the registration. */\n status?: boolean;\n value?: number;\n};\n\n/**\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/payload-helper\n */\nexport type StandardEvents = {\n AddPaymentInfo: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n value?: number;\n };\n AddToCart: {\n content_ids?: string[];\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents\n currency?: string;\n value?: number;\n };\n AddToWishlist: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n value?: number;\n };\n CompleteRegistration: {\n currency?: string;\n value?: number;\n method?: string;\n };\n Contact: {};\n CustomizeProduct: {};\n Donate: {};\n FindLocation: {};\n InitiateCheckout: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n num_items?: number;\n value?: number;\n };\n Lead: {\n currency?: string;\n value?: number;\n };\n Purchase: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency: string; // required\n num_items?: number;\n value: number; // required\n };\n Schedule: {};\n Search: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency?: string;\n search_string?: string;\n value?: number;\n };\n StartTrial: {\n currency?: string;\n predicted_ltv?: number;\n value?: number;\n };\n SubmitApplication: {};\n Subscribe: {\n currency?: string;\n predicted_ltv?: number;\n value?: number;\n };\n ViewContent: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency?: string;\n value?: number;\n };\n};\n\ntype JSONValue =\n | null\n | string\n | number\n | boolean\n | Array<JSONValue>\n | { [value: string]: JSONValue };\n\nexport type PixelId = `${number}`;\nexport type Options = { eventID?: string };\n\n/**\n * reference: https://developers.facebook.com/docs/meta-pixel/reference#standard-events\n *\n * We determine if events are identical based on their ID and name. So, for an event to be deduplicated:\n * - In corresponding events, a Meta Pixel's eventID must match the Conversion API's event_id.\n * - In corresponding events, a Meta Pixel's event must match the Conversion API's event_name.\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nexport interface FBQ {\n /**\n * reference: https://stackoverflow.com/questions/62304291/sending-user-data-parameters-via-pixel\n *\n * Call init the normal default way first:\n * `fbq('init', 'XXXXX')`\n *\n * And at a later point in time, when you have obtained additional user data, you can call init\n * again basically enriching the already running fbq instance with additional data:\n * `fbq('init', 'XXXXX', { external_id: 1234, em: 'abc@abc.com' } )`\n *\n * Only caveat is that you have to send an event after this additional init call, otherwise the\n * provided data will not be sent to Facebook.\n */\n fbq(type: 'init', pixelId: PixelId, parameters?: MatchingParameters): void;\n\n /** Enable Manual Only mode. (value = false) */\n fbq(type: 'set', key: 'autoConfig', value: boolean, pixelId: PixelId): void;\n\n fbq<T extends keyof StandardEvents>(\n type: 'track',\n event: T,\n properties?: StandardEvents[T] & ObjectProperties,\n options?: Options\n ): void;\n\n fbq(\n type: 'trackCustom',\n event: string,\n properties?: Record<string, JSONValue> & ObjectProperties,\n options?: Options\n ): void;\n\n /** https://developers.facebook.com/docs/meta-pixel/guides/track-multiple-events/ */\n fbq<T extends keyof StandardEvents>(\n type: 'trackSingle',\n pixelId: PixelId,\n event: T,\n properties?: StandardEvents[T] & ObjectProperties,\n options?: Options\n ): void;\n\n /** https://developers.facebook.com/docs/meta-pixel/guides/track-multiple-events/ */\n fbq(\n type: 'trackSingleCustom',\n pixelId: PixelId,\n event: string,\n properties?: Record<string, JSONValue> & ObjectProperties,\n options?: Options\n ): void;\n}\n\n/**\n * Please download this CSV filefor examples of properly normalized and hashed data for the\n * parameters below.\n */\nexport function normalize(parameters: MatchingParameters): MatchingParameters {\n return {\n ...parameters,\n em: parameters.em?.toLowerCase().trim(),\n ph: parameters.ph?.replace(/[-+()\\s]/g, '').replace(/^0+/, ''),\n zp: parameters.zp?.split('-').at(0)?.trim(),\n fn: parameters.fn?.toLowerCase().trim(),\n ln: parameters.ln?.toLowerCase().trim(),\n ct: parameters.ct?.toLowerCase().replace(/[s/-]/g, '').trim(),\n st: parameters.st\n ?.toLowerCase()\n .replace(/[s/-/,.]/g, '')\n .trim(),\n country: parameters.country?.toLowerCase().replace(/[s/-]/g, '').trim(),\n };\n}\n\nexport function mapItems(items?: Item[]): ObjectProperties {\n if (!items) return {};\n const categories = Array.from(new Set(items.map((i) => i.item_category).filter(Boolean)));\n const contents: Content[] = items.map(\n ({ item_id, quantity, price, item_name, item_brand, item_category, ..._others }) => ({\n id: item_id,\n quantity: quantity ?? 1,\n item_price: price,\n title: item_name,\n brand: item_brand,\n category: item_category,\n })\n );\n\n return {\n content_category: categories.length === 1 ? categories.at(0) : undefined,\n contents,\n content_ids: contents.map((c) => c.id),\n num_items: items.reduce((acc, i) => acc + (i.quantity ?? 1), 0),\n };\n}\n\ntype Mapped<F extends keyof StandardEvents> = ['track', F, StandardEvents[F] & ObjectProperties];\ntype Missed<F extends string> = ['trackCustom', F, Record<string, JSONValue> & ObjectProperties];\n\nexport function mapFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): Mapped<keyof StandardEvents> | Missed<TrackName<T>> {\n if (name === 'add_payment_info') {\n const p = properties as TrackProperties<'add_payment_info'> | undefined;\n return [\n 'track',\n 'AddPaymentInfo',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'add_to_cart') {\n const p = properties as TrackProperties<'add_to_cart'> | undefined;\n return [\n 'track',\n 'AddToCart',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'add_to_wishlist') {\n const p = properties as TrackProperties<'add_to_wishlist'> | undefined;\n return [\n 'track',\n 'AddToWishlist',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'login') {\n const p = properties as TrackProperties<'login'> | undefined;\n return ['track', 'CompleteRegistration', { method: p?.method }];\n } else if (name === 'contact') {\n return ['track', 'Contact', {}];\n } else if (name === 'customize_product') {\n return ['track', 'CustomizeProduct', {}];\n } else if (name === 'donate') {\n return ['track', 'Donate', {}];\n } else if (name === 'find_location') {\n return ['track', 'FindLocation', {}];\n } else if (name === 'begin_checkout') {\n const p = properties as TrackProperties<'begin_checkout'> | undefined;\n return [\n 'track',\n 'InitiateCheckout',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'generate_lead') {\n const p = properties as TrackProperties<'generate_lead'> | undefined;\n return ['track', 'Lead', { currency: p?.currency, value: p?.value }];\n } else if (name === 'purchase') {\n const p = properties as TrackProperties<'purchase'> | undefined;\n return [\n 'track',\n 'Purchase',\n { currency: p?.currency ?? 'USD', value: p?.value ?? 0, ...mapItems(p?.items) },\n ];\n } else if (name === 'schedule') {\n return ['track', 'Schedule', {}];\n } else if (name === 'search') {\n const p = properties as TrackProperties<'search'> | undefined;\n return ['track', 'Search', { search_string: p?.search_term }];\n } else if (name === 'trial_begin') {\n const p = properties as TrackProperties<'trial_begin'> | undefined;\n return ['track', 'StartTrial', { currency: p?.currency, value: p?.value }];\n } else if (name === 'submit_application') {\n return ['track', 'SubmitApplication', {}];\n } else if (name === 'subscribe') {\n const p = properties as TrackProperties<'subscribe'> | undefined;\n return ['track', 'Subscribe', { currency: p?.currency, value: p?.value }];\n } else if (name === 'view_item') {\n const p = properties as TrackProperties<'view_item'> | undefined;\n return [\n 'track',\n 'ViewContent',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else {\n return ['trackCustom', name, properties ?? {}];\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fbq.mjs","names":["p"],"sources":["../../src/track/fbq.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-empty-object-type */\nimport type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport type Content = {\n id: string;\n quantity: number;\n item_price?: number;\n title?: string;\n description?: string;\n brand?: string;\n category?: string;\n delivery_category?: string;\n};\n\n/**\n * reference: https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nexport type MatchingParameters = {\n /** Email: Unhashed lowercase or hashed SHA-256 */\n em?: string;\n\n /** First Name: Lowercase letters */\n fn?: string;\n\n /** Last Name: Lowercase letters */\n ln?: string;\n\n /** Phone Number: Digits only including country code and area code */\n ph?: string;\n\n /**\n * External ID: Any unique ID from the advertiser, such as loyalty membership ID, user ID, and\n * external cookie ID.\n */\n external_id?: string;\n\n /** Gender: Single lowercase letter, f or m, if unknown, leave blank */\n ge?: 'f' | 'm' | '';\n\n /** Birthdate: Digits only with birth year, month, then day, YYYYMMDD */\n db?: number;\n\n /** City: Lowercase with any spaces removed, e.g. \"menlopark\" */\n ct?: string;\n\n /** State or Province: Lowercase two-letter state or province code, e.g. \"ca\" */\n st?: string;\n\n /** Zip or Postal Code: String */\n zp?: string;\n\n /** Country: Lowercase two-letter country code, e.g. \"us\" */\n country?: string;\n\n /** Client IP Address: Do not hash. */\n client_ip_address?: string;\n\n /** Client User Agent: Do not hash. */\n client_user_agent?: string;\n\n /**\n * Click ID: Do not hash.\n * The Facebook click ID value is stored in the _fbc browser cookie under your domain. See\n * Managing fbc and fbp Parameters for how to get this value or generate this value from a fbclid\n * query parameter.\n *\n * The format is fb.${subdomain_index}.${creation_time}.${fbclid}.\n */\n fbc?: string;\n\n /**\n * Browser ID: Do not hash.\n * The Facebook browser ID value is stored in the _fbp browser cookie under your domain. See\n * Managing fbc and fbp Parameters for how to get this value.\n *\n * The format is fb.${subdomain_index}.${creation_time}.${random_number}.\n */\n fbp?: string;\n\n /**\n * Subscription ID: Do not hash.\n * The subscription ID for the user in this transaction; it is similar to the order ID for an\n * individual product.\n */\n subscription_id?: string;\n\n /**\n * Facebook Login ID: Do not hash.\n * The ID issued by Meta when a person first logs into an instance of an app. This is also known\n * as App-Scoped ID.\n */\n fb_login_id?: number;\n\n /**\n * Lead ID: Do not hash.\n * The ID associated with a lead generated by [Meta's Lead Ads](https://developers.facebook.com/docs/marketing-api/guides/lead-ads).\n */\n lead_id?: number;\n\n /**\n * Install ID: Do not hash.\n * Your install ID. This field represents unique application installation instances.\n * Note: This parameter is for app events only.\n */\n anon_id?: string;\n\n /**\n * Your mobile advertiser ID, the advertising ID from an Android device or the Advertising\n * Identifier (IDFA) from an Apple device.\n */\n madid?: string;\n\n /**\n * Page ID: Do not hash.\n * Your Page ID. Specifies the page ID associated with the event. Use the Facebook page ID of the\n * page associated with the bot.\n */\n page_id?: string;\n\n /**\n * Page Scoped User ID: Do not hash.\n * Specifies the page-scoped user ID associated with the messenger bot that logs the event. Use\n * the page-scoped user ID provided to your webhook.\n */\n page_scoped_user_id?: string;\n\n /**\n * Do not hash.\n * Click ID generated by Meta for ads that click to WhatsApp.\n */\n ctwa_clid?: string;\n\n /**\n * Do not hash.\n * Instagram Account ID that is associated with the business.\n */\n ig_account_id?: string;\n\n /**\n * Do not hash.\n * Users who interact with Instagram are identified by Instagram-Scoped User IDs (IGSID). IGSID\n * can be obtained from this webhook.\n */\n ig_sid?: string;\n};\n\n/**\n * You can include the following predefined object properties with any custom events, and any\n * standard events that support them. Format your parameter object data using JSON. Learn more about\n * event parameters with Blueprint.\n */\nexport type ObjectProperties = {\n content_category?: string;\n content_ids?: string[];\n content_name?: string;\n\n /**\n * Either product or product_group based on the content_ids or contents being passed. If the IDs\n * being passed in content_ids or contents parameter are IDs of products, then the value should be\n * product. If product group IDs are being passed, then the value should be product_group.\n *\n * If no content_type is provided, Meta will match the event to every item that has the same ID,\n * independent of its type.\n */\n content_type?: 'product' | 'product_group' | (string & {});\n contents?: Content[];\n delivery_category?: 'in_store' | 'curbside' | 'home_delivery';\n currency?: string;\n num_items?: number;\n predicted_ltv?: number;\n search_string?: string;\n\n /** Used with the CompleteRegistration event, to show the status of the registration. */\n status?: boolean;\n value?: number;\n};\n\n/**\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/payload-helper\n */\nexport type StandardEvents = {\n AddPaymentInfo: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n value?: number;\n };\n AddToCart: {\n content_ids?: string[];\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents\n currency?: string;\n value?: number;\n };\n AddToWishlist: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n value?: number;\n };\n CompleteRegistration: {\n currency?: string;\n value?: number;\n method?: string;\n };\n Contact: {};\n CustomizeProduct: {};\n Donate: {};\n FindLocation: {};\n InitiateCheckout: {\n content_ids?: string[];\n contents?: Content[];\n currency?: string;\n num_items?: number;\n value?: number;\n };\n Lead: {\n currency?: string;\n value?: number;\n };\n Purchase: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency: string; // required\n num_items?: number;\n value: number; // required\n };\n Schedule: {};\n Search: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency?: string;\n search_string?: string;\n value?: number;\n };\n StartTrial: {\n currency?: string;\n predicted_ltv?: number;\n value?: number;\n };\n SubmitApplication: {};\n Subscribe: {\n currency?: string;\n predicted_ltv?: number;\n value?: number;\n };\n ViewContent: {\n content_ids?: string[]; // Required for Advantage+ catalog ads: contents or content_ids\n content_type?: string;\n contents?: Content[]; // Required for Advantage+ catalog ads: contents or content_ids\n currency?: string;\n value?: number;\n };\n};\n\ntype JSONValue =\n | null\n | string\n | number\n | boolean\n | Array<JSONValue>\n | { [value: string]: JSONValue };\n\nexport type PixelId = `${number}`;\nexport type Options = { eventID?: string };\n\n/**\n * reference: https://developers.facebook.com/docs/meta-pixel/reference#standard-events\n *\n * We determine if events are identical based on their ID and name. So, for an event to be deduplicated:\n * - In corresponding events, a Meta Pixel's eventID must match the Conversion API's event_id.\n * - In corresponding events, a Meta Pixel's event must match the Conversion API's event_name.\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nexport interface FBQ {\n /**\n * reference: https://stackoverflow.com/questions/62304291/sending-user-data-parameters-via-pixel\n *\n * Call init the normal default way first:\n * `fbq('init', 'XXXXX')`\n *\n * And at a later point in time, when you have obtained additional user data, you can call init\n * again basically enriching the already running fbq instance with additional data:\n * `fbq('init', 'XXXXX', { external_id: 1234, em: 'abc@abc.com' } )`\n *\n * Only caveat is that you have to send an event after this additional init call, otherwise the\n * provided data will not be sent to Facebook.\n */\n fbq(type: 'init', pixelId: PixelId, parameters?: MatchingParameters): void;\n\n /** Enable Manual Only mode. (value = false) */\n fbq(type: 'set', key: 'autoConfig', value: boolean, pixelId: PixelId): void;\n\n fbq<T extends keyof StandardEvents>(\n type: 'track',\n event: T,\n properties?: StandardEvents[T] & ObjectProperties,\n options?: Options\n ): void;\n\n fbq(\n type: 'trackCustom',\n event: string,\n properties?: Record<string, JSONValue> & ObjectProperties,\n options?: Options\n ): void;\n\n /** https://developers.facebook.com/docs/meta-pixel/guides/track-multiple-events/ */\n fbq<T extends keyof StandardEvents>(\n type: 'trackSingle',\n pixelId: PixelId,\n event: T,\n properties?: StandardEvents[T] & ObjectProperties,\n options?: Options\n ): void;\n\n /** https://developers.facebook.com/docs/meta-pixel/guides/track-multiple-events/ */\n fbq(\n type: 'trackSingleCustom',\n pixelId: PixelId,\n event: string,\n properties?: Record<string, JSONValue> & ObjectProperties,\n options?: Options\n ): void;\n}\n\n/**\n * Please download this CSV filefor examples of properly normalized and hashed data for the\n * parameters below.\n */\nexport function normalize(parameters: MatchingParameters): MatchingParameters {\n return {\n ...parameters,\n em: parameters.em?.toLowerCase().trim(),\n ph: parameters.ph?.replace(/[-+()\\s]/g, '').replace(/^0+/, ''),\n zp: parameters.zp?.split('-').at(0)?.trim(),\n fn: parameters.fn?.toLowerCase().trim(),\n ln: parameters.ln?.toLowerCase().trim(),\n ct: parameters.ct?.toLowerCase().replace(/[\\s/-]/g, ''),\n st: parameters.st?.toLowerCase().replace(/[\\s/,.-]/g, ''),\n country: parameters.country?.toLowerCase().replace(/[\\s/-]/g, ''),\n };\n}\n\nexport function mapItems(items?: Item[]): ObjectProperties {\n if (!items) return {};\n const categories = Array.from(new Set(items.map((i) => i.item_category).filter(Boolean)));\n const contents: Content[] = items.map(\n ({ item_id, quantity, price, item_name, item_brand, item_category, ..._others }) => ({\n id: item_id,\n quantity: quantity ?? 1,\n item_price: price,\n title: item_name,\n brand: item_brand,\n category: item_category,\n })\n );\n\n return {\n content_category: categories.length === 1 ? categories.at(0) : undefined,\n contents,\n content_ids: contents.map((c) => c.id),\n num_items: items.reduce((acc, i) => acc + (i.quantity ?? 1), 0),\n };\n}\n\ntype Mapped<F extends keyof StandardEvents> = ['track', F, StandardEvents[F] & ObjectProperties];\ntype Missed<F extends string> = ['trackCustom', F, Record<string, JSONValue> & ObjectProperties];\n\nexport function mapFBEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): Mapped<keyof StandardEvents> | Missed<TrackName<T>> {\n if (name === 'add_payment_info') {\n const p = properties as TrackProperties<'add_payment_info'> | undefined;\n return [\n 'track',\n 'AddPaymentInfo',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'add_to_cart') {\n const p = properties as TrackProperties<'add_to_cart'> | undefined;\n return [\n 'track',\n 'AddToCart',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'add_to_wishlist') {\n const p = properties as TrackProperties<'add_to_wishlist'> | undefined;\n return [\n 'track',\n 'AddToWishlist',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'login') {\n const p = properties as TrackProperties<'login'> | undefined;\n return ['track', 'CompleteRegistration', { method: p?.method }];\n } else if (name === 'contact') {\n return ['track', 'Contact', {}];\n } else if (name === 'customize_product') {\n return ['track', 'CustomizeProduct', {}];\n } else if (name === 'donate') {\n return ['track', 'Donate', {}];\n } else if (name === 'find_location') {\n return ['track', 'FindLocation', {}];\n } else if (name === 'begin_checkout') {\n const p = properties as TrackProperties<'begin_checkout'> | undefined;\n return [\n 'track',\n 'InitiateCheckout',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else if (name === 'generate_lead') {\n const p = properties as TrackProperties<'generate_lead'> | undefined;\n return ['track', 'Lead', { currency: p?.currency, value: p?.value }];\n } else if (name === 'purchase') {\n const p = properties as TrackProperties<'purchase'> | undefined;\n return [\n 'track',\n 'Purchase',\n { currency: p?.currency ?? 'USD', value: p?.value ?? 0, ...mapItems(p?.items) },\n ];\n } else if (name === 'schedule') {\n return ['track', 'Schedule', {}];\n } else if (name === 'search') {\n const p = properties as TrackProperties<'search'> | undefined;\n return ['track', 'Search', { search_string: p?.search_term }];\n } else if (name === 'trial_begin') {\n const p = properties as TrackProperties<'trial_begin'> | undefined;\n return ['track', 'StartTrial', { currency: p?.currency, value: p?.value }];\n } else if (name === 'submit_application') {\n return ['track', 'SubmitApplication', {}];\n } else if (name === 'subscribe') {\n const p = properties as TrackProperties<'subscribe'> | undefined;\n return ['track', 'Subscribe', { currency: p?.currency, value: p?.value }];\n } else if (name === 'view_item') {\n const p = properties as TrackProperties<'view_item'> | undefined;\n return [\n 'track',\n 'ViewContent',\n { currency: p?.currency, value: p?.value, ...mapItems(p?.items) },\n ];\n } else {\n return ['trackCustom', name, properties ?? {}];\n }\n}\n"],"mappings":"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"}
|
package/dist/track/index.cjs
CHANGED
|
@@ -6,7 +6,7 @@ const require_third_parties_ignored_events = require("../third-parties/ignored-e
|
|
|
6
6
|
const require_visitor_index = require("../visitor/index.cjs");
|
|
7
7
|
let _shware_utils = require("@shware/utils");
|
|
8
8
|
//#region src/track/index.ts
|
|
9
|
-
const defaultOptions = {
|
|
9
|
+
const defaultOptions = {};
|
|
10
10
|
let tokenBucket;
|
|
11
11
|
/**
|
|
12
12
|
* The rate limiter, built on the first send rather than at module scope: its
|
|
@@ -81,7 +81,7 @@ async function sendEvents(events) {
|
|
|
81
81
|
const eventId = data.at(index)?.id;
|
|
82
82
|
options.onSucceed?.(eventId ? { id: eventId } : void 0);
|
|
83
83
|
index++;
|
|
84
|
-
if (
|
|
84
|
+
if (options.enableThirdPartyTracking === false || require_third_parties_ignored_events.IGNORED_EVENTS.includes(name)) continue;
|
|
85
85
|
require_setup_index.config.thirdPartyTrackers.forEach((tracker) => {
|
|
86
86
|
try {
|
|
87
87
|
tracker(name, properties, eventId);
|
package/dist/track/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","getVisitor","IGNORED_EVENTS","keys"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = { enableThirdPartyTracking: true };\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const firstTimestamp = events[0].timestamp;\n const { id: session_id, started } = getSession().touch(\n Date.parse(firstTimestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started) {\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n // The session began with the event that opened it, not at this moment: a batch held in a\n // frozen tab would otherwise announce its session later than the events inside it.\n timestamp: firstTimestamp,\n });\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\n if (e instanceof Error) console.log(e.message);\n }\n });\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","getVisitor","IGNORED_EVENTS","keys"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const firstTimestamp = events[0].timestamp;\n const { id: session_id, started } = getSession().touch(\n Date.parse(firstTimestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started) {\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n // The session began with the event that opened it, not at this moment: a batch held in a\n // frozen tab would otherwise announce its session later than the events inside it.\n timestamp: firstTimestamp,\n });\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\n if (e instanceof Error) console.log(e.message);\n }\n });\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBA0JG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBA0JG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
|
package/dist/track/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
|
|
|
5
5
|
import { getVisitor } from "../visitor/index.mjs";
|
|
6
6
|
import { TokenBucket, fetch } from "@shware/utils";
|
|
7
7
|
//#region src/track/index.ts
|
|
8
|
-
const defaultOptions = {
|
|
8
|
+
const defaultOptions = {};
|
|
9
9
|
let tokenBucket;
|
|
10
10
|
/**
|
|
11
11
|
* The rate limiter, built on the first send rather than at module scope: its
|
|
@@ -80,7 +80,7 @@ async function sendEvents(events) {
|
|
|
80
80
|
const eventId = data.at(index)?.id;
|
|
81
81
|
options.onSucceed?.(eventId ? { id: eventId } : void 0);
|
|
82
82
|
index++;
|
|
83
|
-
if (
|
|
83
|
+
if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) continue;
|
|
84
84
|
config.thirdPartyTrackers.forEach((tracker) => {
|
|
85
85
|
try {
|
|
86
86
|
tracker(name, properties, eventId);
|
package/dist/track/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = { enableThirdPartyTracking: true };\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const firstTimestamp = events[0].timestamp;\n const { id: session_id, started } = getSession().touch(\n Date.parse(firstTimestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started) {\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n // The session began with the event that opened it, not at this moment: a batch held in a\n // frozen tab would otherwise announce its session later than the events inside it.\n timestamp: firstTimestamp,\n });\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\n if (e instanceof Error) console.log(e.message);\n }\n });\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":";;;;;;;AAeA,MAAM,iBAA+B,EAAE,0BAA0B,KAAK;AAEtE,IAAI;;;;;;;AAQJ,SAAS,iBAAiB;CACxB,OAAQ,gBAAgB,IAAI,YAAY;EAAE,MAAM;EAAG,UAAU;EAAI,WAAW;CAAE,CAAC;AACjF;;;;;;;;;;AAqBA,eAAe,cAAkC;CAC/C,IAAI;EACF,OAAO,MAAM,OAAO,QAAQ;CAC9B,SAAS,GAAY;EACnB,IAAI,aAAa,OAAO,QAAQ,IAAI,EAAE,OAAO;EAC7C,OAAO,MAAM,QAAQ,CAAC;CACxB;AACF;AAEA,eAAe,WAAW,QAAgB;CACxC,IAAI;EACF,IAAI,OAAO,WAAW,GAAG;EAMzB,MAAM,iBAAiB,OAAO,EAAE,CAAC;EACjC,MAAM,EAAE,IAAI,YAAY,YAAY,WAAW,CAAC,CAAC,MAC/C,KAAK,MAAM,cAAc,GACzB,KAAK,MAAM,OAAO,OAAO,SAAS,EAAE,CAAC,SAAS,CAChD;EACA,IAAI,SACF,OAAO,QAAQ;GACb,MAAM;GACN,YAAY,CAAC;GACb,SAAS,EAAE,0BAA0B,MAAM;GAC3C,MAAM,YAAY;GAGlB,WAAW;EACb,CAAC;EAGH,MAAM,eAAe,CAAC,CAAC,aAAa;EAEpC,MAAM,cAAc,MAAM,WAAW,EAAA,CAAG;EAExC,MAAM,MAA2B,MAAM,QAAQ,IAC7C,OAAO,IAAI,OAAO,WAAW;GAC3B,MAAM,MAAM;GACZ,YAAY,MAAM;GAClB,MAAM,MAAM,MAAM;GAClB;GACA;GACA,UAAU,OAAO;GACjB,aAAa,OAAO;GACpB,WAAW,MAAM;EACnB,EAAE,CACJ;EAEA,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,UAAU;GACxD,QAAQ;GACR,aAAa;GACb,SAAS,MAAM,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,GAAG;EAC1B,CAAC;EAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,+BAA+B,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;EAG3F,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,QAAQ;EACZ,OAAO,OAAO,SAAS,GAAG;GACxB,MAAM,QAAQ,OAAO,MAAM;GAC3B,IAAI,CAAC,OAAO;IACV;IACA;GACF;GACA,MAAM,EAAE,SAAS,MAAM,eAAe;GACtC,MAAM,UAAU,KAAK,GAAG,KAAK,CAAC,EAAE;GAChC,QAAQ,YAAY,UAAU,EAAE,IAAI,QAAQ,IAAI,KAAA,CAAS;GACzD;GACA,IAAI,CAAC,QAAQ,4BAA4B,eAAe,SAAS,IAAI,GACnE;GAEF,OAAO,mBAAmB,SAAS,YAAY;IAC7C,IAAI;KACF,QAAQ,MAAM,YAAY,OAAO;IACnC,SAAS,GAAY;KAKnB,IAAI,aAAa,OAAO,QAAQ,IAAI,EAAE,OAAO;IAC/C;GACF,CAAC;EACH;CACF,SAAS,GAAY;EACnB,IAAI,aAAa,OAAO,QAAQ,IAAI,EAAE,OAAO;EAC7C,OAAO,SAAS,UAAU,MAAM,QAAQ,UAAU,CAAC,CAAC;CACtD;AACF;AAEA,MAAM,QAAQ;AACd,MAAM,QAAQ;AACd,MAAM,OAAe,CAAC;AACtB,IAAI,QAA8C;;;;;AAMlD,SAAS,QAAQ;CACf,IAAI,OAAO;EACT,aAAa,KAAK;EAClB,QAAQ;CACV;CACA,IAAI,KAAK,WAAW,GAAG;CACvB,MAAM,OAAO,CAAC,GAAG,IAAI;CACrB,KAAK,SAAS;CACd,WAAgB,IAAI;AACtB;AAEA,SAAgB,MACd,MACA,YACA,UAAwB,gBACxB;CACA,KAAK,KAAK;EACR;EACA;EACA;EACA,MAAM,YAAY;EAClB,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;CACpC,CAAC;CACD,IAAI,KAAK,UAAU,OAAO;EACxB,MAAM;EACN;CACF;CACA,IAAI,OAAO,aAAa,KAAK;CAC7B,QAAQ,WAAW,OAAO,KAAK;AACjC;AAEA,eAAsB,WACpB,MACA,YACA,UAAwB,gBACxB;CACA,MAAM,WAAW,CACf;EAAE;EAAM;EAAY;EAAS,MAAM,YAAY;EAAG,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;CAAE,CACxF,CAAC;AACH;AAEA,SAAgB,WACd,MACA,YACA;CAKA,MAAM,SAAS,OAAO,QAAQ,QAAQ,KAAK,UAAU;CACrD,MAAM,aAAa,MAAM,SAAS,OAAO,UAAU,WAAW,cAAc,SAAS,KAAA;CACrF,IAAI,CAAC,YAAY;CAEjB,MAAM,MAA2B,CAC/B;EACE;EACA;EAGA,MAAM,MAAM,QAAQ,CAAC;EACrB;EACA,YAAY,WAAW,CAAC,CAAC,OAAO;EAChC,UAAU,OAAO;EACjB,aAAa,OAAO;EACpB,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;CACpC,CACF;CACA,MAAM,OAAO,IAAI,KAAK,CAAC,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,mBAAmB,CAAC;CAEzE,IADgB,UAAU,WAAW,GAAG,OAAO,SAAS,UAAU,IACxD,GAAG;CACb,QAAQ,KAAK,yBAAyB,MAAM,UAAU;AACxD"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const firstTimestamp = events[0].timestamp;\n const { id: session_id, started } = getSession().touch(\n Date.parse(firstTimestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started) {\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n // The session began with the event that opened it, not at this moment: a batch held in a\n // frozen tab would otherwise announce its session later than the events inside it.\n timestamp: firstTimestamp,\n });\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\n if (e instanceof Error) console.log(e.message);\n }\n });\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shware/analytics",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -134,10 +134,14 @@
|
|
|
134
134
|
},
|
|
135
135
|
"devDependencies": {
|
|
136
136
|
"@repo/typescript-config": "0.0.0",
|
|
137
|
+
"@testing-library/react": "^16.3.2",
|
|
137
138
|
"@types/facebook-nodejs-business-sdk": "^24.0.0",
|
|
138
139
|
"@types/node": "^26.3.0",
|
|
139
140
|
"@types/react": "^19.2.18",
|
|
141
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
140
142
|
"bowser": "^2.14.1",
|
|
143
|
+
"jsdom": "^30.0.1",
|
|
144
|
+
"react-dom": "^19.2.8",
|
|
141
145
|
"typescript": "^7.0.2",
|
|
142
146
|
"vitest": "^4.1.11"
|
|
143
147
|
},
|