@shware/analytics 5.1.1 → 5.1.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/dist/hooks/use-app-analytics.cjs +7 -6
- package/dist/hooks/use-app-analytics.cjs.map +1 -1
- package/dist/hooks/use-app-analytics.mjs +4 -3
- package/dist/hooks/use-app-analytics.mjs.map +1 -1
- package/dist/hooks/use-web-analytics.cjs +14 -13
- package/dist/hooks/use-web-analytics.cjs.map +1 -1
- package/dist/hooks/use-web-analytics.mjs +7 -6
- package/dist/hooks/use-web-analytics.mjs.map +1 -1
- package/dist/setup/session.cjs +28 -2
- package/dist/setup/session.cjs.map +1 -1
- package/dist/setup/session.d.cts +25 -2
- package/dist/setup/session.d.cts.map +1 -1
- package/dist/setup/session.d.mts +25 -2
- package/dist/setup/session.d.mts.map +1 -1
- package/dist/setup/session.mjs +28 -2
- package/dist/setup/session.mjs.map +1 -1
- package/dist/track/index.cjs +23 -12
- 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 +18 -7
- package/dist/track/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ function sendFirstOpen(pathname) {
|
|
|
16
16
|
require_setup_index.config.storage.setItem(require_constants_storage.keys.first_open_time, (/* @__PURE__ */ new Date()).toISOString());
|
|
17
17
|
}
|
|
18
18
|
function sendUserEngagement() {
|
|
19
|
-
const engagement_time_msec = require_setup_session.
|
|
19
|
+
const engagement_time_msec = require_setup_session.getSession().flush();
|
|
20
20
|
if (engagement_time_msec <= 0) return;
|
|
21
21
|
require_track_index.track("user_engagement", {
|
|
22
22
|
engagement_time_msec,
|
|
@@ -26,13 +26,14 @@ function sendUserEngagement() {
|
|
|
26
26
|
function useAppAnalytics(pathname) {
|
|
27
27
|
const prevPathname = require_hooks_use_previous.usePrevious(pathname);
|
|
28
28
|
(0, react.useEffect)(() => {
|
|
29
|
+
const session = require_setup_session.getSession();
|
|
29
30
|
sendFirstOpen(pathname);
|
|
30
31
|
require_track_index.track("session_start", void 0);
|
|
31
32
|
const subscription = react_native.AppState.addEventListener("change", (state) => {
|
|
32
|
-
|
|
33
|
-
if (state === "active" && !
|
|
34
|
-
else if (state !== "active" &&
|
|
35
|
-
|
|
33
|
+
session.updateAccumulator();
|
|
34
|
+
if (state === "active" && !session.isActive()) session.updateActive(true);
|
|
35
|
+
else if (state !== "active" && session.isActive()) {
|
|
36
|
+
session.updateActive(false);
|
|
36
37
|
sendUserEngagement();
|
|
37
38
|
}
|
|
38
39
|
});
|
|
@@ -43,7 +44,7 @@ function useAppAnalytics(pathname) {
|
|
|
43
44
|
screen_name: pathname,
|
|
44
45
|
screen_class: pathname,
|
|
45
46
|
previous_screen_class: prevPathname ?? void 0,
|
|
46
|
-
engagement_time_msec: prevPathname ? require_setup_session.
|
|
47
|
+
engagement_time_msec: prevPathname ? require_setup_session.getSession().flush() : void 0
|
|
47
48
|
});
|
|
48
49
|
}, [pathname]);
|
|
49
50
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-app-analytics.cjs","names":["config","keys","
|
|
1
|
+
{"version":3,"file":"use-app-analytics.cjs","names":["config","keys","getSession","usePrevious","AppState"],"sources":["../../src/hooks/use-app-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { AppState } from 'react-native';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstOpen(pathname: string) {\n if (config.storage.getItem(keys.first_open_time)) return;\n track('first_open', { screen_name: pathname, screen_class: pathname });\n config.storage.setItem(keys.first_open_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('user_engagement', { engagement_time_msec, trigger: 'background' });\n}\n\nexport function useAppAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n useEffect(() => {\n const session = getSession();\n\n sendFirstOpen(pathname);\n track('session_start', undefined);\n\n const subscription = AppState.addEventListener('change', (state) => {\n session.updateAccumulator();\n // when returning to the foreground from the background\n if (state === 'active' && !session.isActive()) {\n session.updateActive(true);\n }\n // when entering the background\n else if (state !== 'active' && session.isActive()) {\n session.updateActive(false);\n sendUserEngagement();\n }\n });\n\n return () => subscription.remove();\n }, []);\n\n // when the screen is switched, the engagement time of the previous screen is recorded\n useEffect(() => {\n track('screen_view', {\n screen_name: pathname,\n screen_class: pathname,\n previous_screen_class: prevPathname ?? undefined,\n engagement_time_msec: prevPathname ? getSession().flush() : undefined,\n });\n }, [pathname]);\n}\n"],"mappings":";;;;;;;;;AAQA,SAAS,cAAc,UAAkB;CACvC,IAAIA,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,eAAe,GAAG;CAClD,oBAAA,MAAM,cAAc;EAAE,aAAa;EAAU,cAAc;CAAS,CAAC;CACrE,oBAAA,OAAO,QAAQ,QAAQA,0BAAAA,KAAK,kCAAiB,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;AACvE;AAEA,SAAS,qBAAqB;CAC5B,MAAM,uBAAuBC,sBAAAA,WAAW,CAAC,CAAC,MAAM;CAChD,IAAI,wBAAwB,GAAG;CAC/B,oBAAA,MAAM,mBAAmB;EAAE;EAAsB,SAAS;CAAa,CAAC;AAC1E;AAEA,SAAgB,gBAAgB,UAAkB;CAChD,MAAM,eAAeC,2BAAAA,YAAY,QAAQ;CAEzC,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,UAAUD,sBAAAA,WAAW;EAE3B,cAAc,QAAQ;EACtB,oBAAA,MAAM,iBAAiB,KAAA,CAAS;EAEhC,MAAM,eAAeE,aAAAA,SAAS,iBAAiB,WAAW,UAAU;GAClE,QAAQ,kBAAkB;GAE1B,IAAI,UAAU,YAAY,CAAC,QAAQ,SAAS,GAC1C,QAAQ,aAAa,IAAI;QAGtB,IAAI,UAAU,YAAY,QAAQ,SAAS,GAAG;IACjD,QAAQ,aAAa,KAAK;IAC1B,mBAAmB;GACrB;EACF,CAAC;EAED,aAAa,aAAa,OAAO;CACnC,GAAG,CAAC,CAAC;CAGL,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,oBAAA,MAAM,eAAe;GACnB,aAAa;GACb,cAAc;GACd,uBAAuB,gBAAgB,KAAA;GACvC,sBAAsB,eAAeF,sBAAAA,WAAW,CAAC,CAAC,MAAM,IAAI,KAAA;EAC9D,CAAC;CACH,GAAG,CAAC,QAAQ,CAAC;AACf"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { config } from "../setup/index.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { getSession } from "../setup/session.mjs";
|
|
3
3
|
import { keys } from "../constants/storage.mjs";
|
|
4
4
|
import { track } from "../track/index.mjs";
|
|
5
5
|
import { usePrevious } from "./use-previous.mjs";
|
|
@@ -15,7 +15,7 @@ function sendFirstOpen(pathname) {
|
|
|
15
15
|
config.storage.setItem(keys.first_open_time, (/* @__PURE__ */ new Date()).toISOString());
|
|
16
16
|
}
|
|
17
17
|
function sendUserEngagement() {
|
|
18
|
-
const engagement_time_msec =
|
|
18
|
+
const engagement_time_msec = getSession().flush();
|
|
19
19
|
if (engagement_time_msec <= 0) return;
|
|
20
20
|
track("user_engagement", {
|
|
21
21
|
engagement_time_msec,
|
|
@@ -25,6 +25,7 @@ function sendUserEngagement() {
|
|
|
25
25
|
function useAppAnalytics(pathname) {
|
|
26
26
|
const prevPathname = usePrevious(pathname);
|
|
27
27
|
useEffect(() => {
|
|
28
|
+
const session = getSession();
|
|
28
29
|
sendFirstOpen(pathname);
|
|
29
30
|
track("session_start", void 0);
|
|
30
31
|
const subscription = AppState.addEventListener("change", (state) => {
|
|
@@ -42,7 +43,7 @@ function useAppAnalytics(pathname) {
|
|
|
42
43
|
screen_name: pathname,
|
|
43
44
|
screen_class: pathname,
|
|
44
45
|
previous_screen_class: prevPathname ?? void 0,
|
|
45
|
-
engagement_time_msec: prevPathname ?
|
|
46
|
+
engagement_time_msec: prevPathname ? getSession().flush() : void 0
|
|
46
47
|
});
|
|
47
48
|
}, [pathname]);
|
|
48
49
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-app-analytics.mjs","names":[],"sources":["../../src/hooks/use-app-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { AppState } from 'react-native';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport {
|
|
1
|
+
{"version":3,"file":"use-app-analytics.mjs","names":[],"sources":["../../src/hooks/use-app-analytics.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { AppState } from 'react-native';\nimport { keys } from '../constants/storage';\nimport { config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { track } from '../track/index';\nimport { usePrevious } from './use-previous';\n\nfunction sendFirstOpen(pathname: string) {\n if (config.storage.getItem(keys.first_open_time)) return;\n track('first_open', { screen_name: pathname, screen_class: pathname });\n config.storage.setItem(keys.first_open_time, new Date().toISOString());\n}\n\nfunction sendUserEngagement() {\n const engagement_time_msec = getSession().flush();\n if (engagement_time_msec <= 0) return;\n track('user_engagement', { engagement_time_msec, trigger: 'background' });\n}\n\nexport function useAppAnalytics(pathname: string) {\n const prevPathname = usePrevious(pathname);\n\n useEffect(() => {\n const session = getSession();\n\n sendFirstOpen(pathname);\n track('session_start', undefined);\n\n const subscription = AppState.addEventListener('change', (state) => {\n session.updateAccumulator();\n // when returning to the foreground from the background\n if (state === 'active' && !session.isActive()) {\n session.updateActive(true);\n }\n // when entering the background\n else if (state !== 'active' && session.isActive()) {\n session.updateActive(false);\n sendUserEngagement();\n }\n });\n\n return () => subscription.remove();\n }, []);\n\n // when the screen is switched, the engagement time of the previous screen is recorded\n useEffect(() => {\n track('screen_view', {\n screen_name: pathname,\n screen_class: pathname,\n previous_screen_class: prevPathname ?? undefined,\n engagement_time_msec: prevPathname ? getSession().flush() : undefined,\n });\n }, [pathname]);\n}\n"],"mappings":";;;;;;;;AAQA,SAAS,cAAc,UAAkB;CACvC,IAAI,OAAO,QAAQ,QAAQ,KAAK,eAAe,GAAG;CAClD,MAAM,cAAc;EAAE,aAAa;EAAU,cAAc;CAAS,CAAC;CACrE,OAAO,QAAQ,QAAQ,KAAK,kCAAiB,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;AACvE;AAEA,SAAS,qBAAqB;CAC5B,MAAM,uBAAuB,WAAW,CAAC,CAAC,MAAM;CAChD,IAAI,wBAAwB,GAAG;CAC/B,MAAM,mBAAmB;EAAE;EAAsB,SAAS;CAAa,CAAC;AAC1E;AAEA,SAAgB,gBAAgB,UAAkB;CAChD,MAAM,eAAe,YAAY,QAAQ;CAEzC,gBAAgB;EACd,MAAM,UAAU,WAAW;EAE3B,cAAc,QAAQ;EACtB,MAAM,iBAAiB,KAAA,CAAS;EAEhC,MAAM,eAAe,SAAS,iBAAiB,WAAW,UAAU;GAClE,QAAQ,kBAAkB;GAE1B,IAAI,UAAU,YAAY,CAAC,QAAQ,SAAS,GAC1C,QAAQ,aAAa,IAAI;QAGtB,IAAI,UAAU,YAAY,QAAQ,SAAS,GAAG;IACjD,QAAQ,aAAa,KAAK;IAC1B,mBAAmB;GACrB;EACF,CAAC;EAED,aAAa,aAAa,OAAO;CACnC,GAAG,CAAC,CAAC;CAGL,gBAAgB;EACd,MAAM,eAAe;GACnB,aAAa;GACb,cAAc;GACd,uBAAuB,gBAAgB,KAAA;GACvC,sBAAsB,eAAe,WAAW,CAAC,CAAC,MAAM,IAAI,KAAA;EAC9D,CAAC;CACH,GAAG,CAAC,QAAQ,CAAC;AACf"}
|
|
@@ -18,7 +18,7 @@ function sendFirstVisit(pathname) {
|
|
|
18
18
|
require_setup_index.config.storage.setItem(require_constants_storage.keys.first_visit_time, (/* @__PURE__ */ new Date()).toISOString());
|
|
19
19
|
}
|
|
20
20
|
function sendUserEngagement(trigger) {
|
|
21
|
-
const engagement_time_msec = require_setup_session.
|
|
21
|
+
const engagement_time_msec = require_setup_session.getSession().flush();
|
|
22
22
|
if (engagement_time_msec <= 0) return;
|
|
23
23
|
require_track_index.sendBeacon("user_engagement", {
|
|
24
24
|
engagement_time_msec,
|
|
@@ -26,7 +26,7 @@ function sendUserEngagement(trigger) {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
function sendScroll() {
|
|
29
|
-
const engagement_time_msec = require_setup_session.
|
|
29
|
+
const engagement_time_msec = require_setup_session.getSession().flush();
|
|
30
30
|
if (engagement_time_msec <= 0) return;
|
|
31
31
|
require_track_index.track("scroll", { engagement_time_msec });
|
|
32
32
|
}
|
|
@@ -38,11 +38,11 @@ function getScrollPercent() {
|
|
|
38
38
|
return (scrollTop + windowHeight) * 100 / docHeight;
|
|
39
39
|
}
|
|
40
40
|
function onPageHide() {
|
|
41
|
-
require_setup_session.
|
|
41
|
+
require_setup_session.getSession().pagehide();
|
|
42
42
|
sendUserEngagement("pagehide");
|
|
43
43
|
}
|
|
44
44
|
function onVisibilityChange() {
|
|
45
|
-
require_setup_session.
|
|
45
|
+
require_setup_session.getSession().visibilitychange(document.visibilityState);
|
|
46
46
|
if (document.visibilityState === "hidden") sendUserEngagement("visibilitychange");
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
@@ -57,10 +57,11 @@ function useWebAnalytics(pathname) {
|
|
|
57
57
|
hasSendScroll.current = false;
|
|
58
58
|
}, [pathname]);
|
|
59
59
|
(0, react.useEffect)(() => {
|
|
60
|
+
const session = require_setup_session.getSession();
|
|
60
61
|
sendFirstVisit(pathname);
|
|
61
62
|
require_track_index.track("session_start", void 0);
|
|
62
63
|
const onScroll = (0, _shware_utils.throttle)(() => {
|
|
63
|
-
|
|
64
|
+
session.updateAccumulator();
|
|
64
65
|
if (hasSendScroll.current) return;
|
|
65
66
|
if (getScrollPercent() < 90) return;
|
|
66
67
|
hasSendScroll.current = true;
|
|
@@ -71,11 +72,11 @@ function useWebAnalytics(pathname) {
|
|
|
71
72
|
"keydown",
|
|
72
73
|
"touchstart"
|
|
73
74
|
];
|
|
74
|
-
const checkpoint = (0, _shware_utils.throttle)(
|
|
75
|
-
window.addEventListener("focus",
|
|
76
|
-
window.addEventListener("blur",
|
|
75
|
+
const checkpoint = (0, _shware_utils.throttle)(session.updateAccumulator, 1e3);
|
|
76
|
+
window.addEventListener("focus", session.focus);
|
|
77
|
+
window.addEventListener("blur", session.blur);
|
|
77
78
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
78
|
-
window.addEventListener("pageshow",
|
|
79
|
+
window.addEventListener("pageshow", session.pageshow);
|
|
79
80
|
window.addEventListener("pagehide", onPageHide);
|
|
80
81
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
81
82
|
checkpointEvents.forEach((e) => {
|
|
@@ -85,10 +86,10 @@ function useWebAnalytics(pathname) {
|
|
|
85
86
|
});
|
|
86
87
|
});
|
|
87
88
|
return () => {
|
|
88
|
-
window.removeEventListener("focus",
|
|
89
|
-
window.removeEventListener("blur",
|
|
89
|
+
window.removeEventListener("focus", session.focus);
|
|
90
|
+
window.removeEventListener("blur", session.blur);
|
|
90
91
|
window.removeEventListener("scroll", onScroll);
|
|
91
|
-
window.removeEventListener("pageshow",
|
|
92
|
+
window.removeEventListener("pageshow", session.pageshow);
|
|
92
93
|
window.removeEventListener("pagehide", onPageHide);
|
|
93
94
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
94
95
|
checkpointEvents.forEach((e) => window.removeEventListener(e, checkpoint));
|
|
@@ -103,7 +104,7 @@ function useWebAnalytics(pathname) {
|
|
|
103
104
|
page_referrer: document.referrer,
|
|
104
105
|
page_location: window.location.href,
|
|
105
106
|
previous_page_path: prevPathname ?? void 0,
|
|
106
|
-
engagement_time_msec: prevPathname ? require_setup_session.
|
|
107
|
+
engagement_time_msec: prevPathname ? require_setup_session.getSession().flush() : void 0
|
|
107
108
|
});
|
|
108
109
|
}, [pathname]);
|
|
109
110
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-analytics.cjs","names":["config","keys","
|
|
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 track('session_start', undefined);\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 = true;\n 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));\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,5 +1,5 @@
|
|
|
1
1
|
import { config } from "../setup/index.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { getSession } from "../setup/session.mjs";
|
|
3
3
|
import { keys } from "../constants/storage.mjs";
|
|
4
4
|
import { sendBeacon, track } from "../track/index.mjs";
|
|
5
5
|
import { usePrevious } from "./use-previous.mjs";
|
|
@@ -17,7 +17,7 @@ function sendFirstVisit(pathname) {
|
|
|
17
17
|
config.storage.setItem(keys.first_visit_time, (/* @__PURE__ */ new Date()).toISOString());
|
|
18
18
|
}
|
|
19
19
|
function sendUserEngagement(trigger) {
|
|
20
|
-
const engagement_time_msec =
|
|
20
|
+
const engagement_time_msec = getSession().flush();
|
|
21
21
|
if (engagement_time_msec <= 0) return;
|
|
22
22
|
sendBeacon("user_engagement", {
|
|
23
23
|
engagement_time_msec,
|
|
@@ -25,7 +25,7 @@ function sendUserEngagement(trigger) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
function sendScroll() {
|
|
28
|
-
const engagement_time_msec =
|
|
28
|
+
const engagement_time_msec = getSession().flush();
|
|
29
29
|
if (engagement_time_msec <= 0) return;
|
|
30
30
|
track("scroll", { engagement_time_msec });
|
|
31
31
|
}
|
|
@@ -37,11 +37,11 @@ function getScrollPercent() {
|
|
|
37
37
|
return (scrollTop + windowHeight) * 100 / docHeight;
|
|
38
38
|
}
|
|
39
39
|
function onPageHide() {
|
|
40
|
-
|
|
40
|
+
getSession().pagehide();
|
|
41
41
|
sendUserEngagement("pagehide");
|
|
42
42
|
}
|
|
43
43
|
function onVisibilityChange() {
|
|
44
|
-
|
|
44
|
+
getSession().visibilitychange(document.visibilityState);
|
|
45
45
|
if (document.visibilityState === "hidden") sendUserEngagement("visibilitychange");
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
@@ -56,6 +56,7 @@ function useWebAnalytics(pathname) {
|
|
|
56
56
|
hasSendScroll.current = false;
|
|
57
57
|
}, [pathname]);
|
|
58
58
|
useEffect(() => {
|
|
59
|
+
const session = getSession();
|
|
59
60
|
sendFirstVisit(pathname);
|
|
60
61
|
track("session_start", void 0);
|
|
61
62
|
const onScroll = throttle(() => {
|
|
@@ -102,7 +103,7 @@ function useWebAnalytics(pathname) {
|
|
|
102
103
|
page_referrer: document.referrer,
|
|
103
104
|
page_location: window.location.href,
|
|
104
105
|
previous_page_path: prevPathname ?? void 0,
|
|
105
|
-
engagement_time_msec: prevPathname ?
|
|
106
|
+
engagement_time_msec: prevPathname ? getSession().flush() : void 0
|
|
106
107
|
});
|
|
107
108
|
}, [pathname]);
|
|
108
109
|
}
|
|
@@ -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 {
|
|
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 track('session_start', undefined);\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 = true;\n 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));\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":";;;;;;;;AAQA,SAAS,eAAe,UAAkB;CACxC,IAAI,OAAO,QAAQ,QAAQ,KAAK,gBAAgB,GAAG;CACnD,MAAM,eAAe;EACnB,WAAW;EACX,YAAY,SAAS;EACrB,eAAe,SAAS;EACxB,eAAe,OAAO,SAAS;CACjC,CAAC;CACD,OAAO,QAAQ,QAAQ,KAAK,mCAAkB,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC;AACxE;AAEA,SAAS,mBAAmB,SAA0C;CACpE,MAAM,uBAAuB,WAAW,CAAC,CAAC,MAAM;CAChD,IAAI,wBAAwB,GAAG;CAC/B,WAAW,mBAAmB;EAAE;EAAsB;CAAQ,CAAC;AACjE;AAEA,SAAS,aAAa;CACpB,MAAM,uBAAuB,WAAW,CAAC,CAAC,MAAM;CAChD,IAAI,wBAAwB,GAAG;CAC/B,MAAM,UAAU,EAAE,qBAAqB,CAAC;AAC1C;AAEA,SAAS,mBAAmB;CAC1B,MAAM,YAAY,OAAO,WAAW,SAAS,gBAAgB;CAC7D,MAAM,eAAe,OAAO;CAC5B,MAAM,YAAY,SAAS,gBAAgB;CAC3C,IAAI,cAAc,GAAG,OAAO;CAC5B,QAAS,YAAY,gBAAgB,MAAO;AAC9C;AAEA,SAAS,aAAa;CACpB,WAAW,CAAC,CAAC,SAAS;CACtB,mBAAmB,UAAU;AAC/B;AAEA,SAAS,qBAAqB;CAC5B,WAAW,CAAC,CAAC,iBAAiB,SAAS,eAAe;CACtD,IAAI,SAAS,oBAAoB,UAC/B,mBAAmB,kBAAkB;AAEzC;;;;;;AAOA,SAAgB,gBAAgB,UAAkB;CAChD,MAAM,eAAe,YAAY,QAAQ;CAGzC,MAAM,gBAAgB,OAAO,KAAK;CAClC,gBAAgB;EACd,cAAc,UAAU;CAC1B,GAAG,CAAC,QAAQ,CAAC;CAEb,gBAAgB;EAGd,MAAM,UAAU,WAAW;EAE3B,eAAe,QAAQ;EACvB,MAAM,iBAAiB,KAAA,CAAS;EAEhC,MAAM,WAAW,eAAe;GAC9B,QAAQ,kBAAkB;GAC1B,IAAI,cAAc,SAAS;GAE3B,IAAI,iBAAiB,IAAI,IAAI;GAC7B,cAAc,UAAU;GACxB,WAAW;EACb,GAAG,GAAG;EAEN,MAAM,mBAAmB;GAAC;GAAa;GAAW;EAAY;EAC9D,MAAM,aAAa,SAAS,QAAQ,mBAAmB,GAAI;EAE3D,OAAO,iBAAiB,SAAS,QAAQ,KAAK;EAC9C,OAAO,iBAAiB,QAAQ,QAAQ,IAAI;EAC5C,OAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;EAC7D,OAAO,iBAAiB,YAAY,QAAQ,QAAQ;EACpD,OAAO,iBAAiB,YAAY,UAAU;EAC9C,SAAS,iBAAiB,oBAAoB,kBAAkB;EAGhE,iBAAiB,SAAS,MAAM;GAC9B,OAAO,iBAAiB,GAAG,YAAY;IAAE,SAAS;IAAM,SAAS;GAAK,CAAC;EACzE,CAAC;EAED,aAAa;GACX,OAAO,oBAAoB,SAAS,QAAQ,KAAK;GACjD,OAAO,oBAAoB,QAAQ,QAAQ,IAAI;GAC/C,OAAO,oBAAoB,UAAU,QAAQ;GAC7C,OAAO,oBAAoB,YAAY,QAAQ,QAAQ;GACvD,OAAO,oBAAoB,YAAY,UAAU;GACjD,SAAS,oBAAoB,oBAAoB,kBAAkB;GACnE,iBAAiB,SAAS,MAAM,OAAO,oBAAoB,GAAG,UAAU,CAAC;GAEzE,SAAS,OAAO;GAChB,WAAW,OAAO;EACpB;CACF,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,MAAM,aAAa;GACjB,WAAW;GACX,YAAY,SAAS;GACrB,eAAe,SAAS;GACxB,eAAe,OAAO,SAAS;GAC/B,oBAAoB,gBAAgB,KAAA;GACpC,sBAAsB,eAAe,WAAW,CAAC,CAAC,MAAM,IAAI,KAAA;EAC9D,CAAC;CACH,GAAG,CAAC,QAAQ,CAAC;AACf"}
|
package/dist/setup/session.cjs
CHANGED
|
@@ -65,9 +65,35 @@ var Session = class {
|
|
|
65
65
|
this.focused = typeof document !== "undefined" ? document.hasFocus() : true;
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
|
-
|
|
68
|
+
let session;
|
|
69
|
+
/**
|
|
70
|
+
* The session, built the first time something asks for it.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately not a module-scope `new Session()`. The constructor calls
|
|
73
|
+
* `uuidv7()`, `uuid` draws its bytes from `crypto.getRandomValues`, and
|
|
74
|
+
* Cloudflare Workers reject that outside a request handler:
|
|
75
|
+
*
|
|
76
|
+
* Disallowed operation called within global scope. Asynchronous I/O
|
|
77
|
+
* (ex: fetch() or connect()), setting a timeout, and generating random
|
|
78
|
+
* values are not allowed within global scope.
|
|
79
|
+
*
|
|
80
|
+
* Module scope in a Worker is evaluated once when the isolate boots and is
|
|
81
|
+
* then shared by every request that isolate serves, so a random value drawn
|
|
82
|
+
* there would be the same for all of them — which is why the runtime refuses
|
|
83
|
+
* to produce one. A host that server-renders on Workers reaches this module
|
|
84
|
+
* through `track()` on the server as well, and the throw happened as the
|
|
85
|
+
* isolate booted, taking down every route before a component rendered.
|
|
86
|
+
*
|
|
87
|
+
* Everything here is per-visitor browser or app state, so deferring the
|
|
88
|
+
* construction costs nothing and buys two things: the server bundle can be
|
|
89
|
+
* evaluated, and `startTime` marks when the session actually began rather
|
|
90
|
+
* than when the isolate happened to start.
|
|
91
|
+
*/
|
|
92
|
+
function getSession() {
|
|
93
|
+
return session ??= new Session();
|
|
94
|
+
}
|
|
69
95
|
//#endregion
|
|
70
96
|
exports.SESSION_TIMEOUT = SESSION_TIMEOUT;
|
|
71
|
-
exports.
|
|
97
|
+
exports.getSession = getSession;
|
|
72
98
|
|
|
73
99
|
//# sourceMappingURL=session.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.cjs","names":[],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\nclass Session {\n private id: string;\n private startTime: number;\n private lastActiveTime: number;\n private accumulatedTime: number;\n\n private active: boolean;\n private visible: boolean;\n private focused: boolean;\n\n constructor() {\n this.id = uuidv7();\n this.startTime = Date.now();\n this.lastActiveTime = 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 getId = () => this.id;\n\n isActive = () => this.active;\n isVisible = () => this.visible;\n isFocused = () => this.focused;\n isExpired = () => Date.now() - this.lastActiveTime > SESSION_TIMEOUT;\n\n updateLastActiveTime = () => {\n this.lastActiveTime = Date.now();\n };\n\n updateActive = (active: boolean) => {\n this.active = active;\n };\n\n refresh = () => {\n this.id = uuidv7();\n this.lastActiveTime = Date.now();\n };\n\n updateAccumulator = () => {\n const now = Date.now();\n if (this.focused && this.visible && this.active) {\n const delta = now - this.startTime;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n this.accumulatedTime += delta;\n }\n }\n this.startTime = now;\n };\n\n focus = () => {\n this.updateAccumulator();\n this.updateLastActiveTime();\n this.focused = true;\n };\n\n blur = () => {\n this.updateAccumulator();\n this.focused = false;\n };\n\n pageshow = () => {\n this.updateAccumulator();\n this.updateLastActiveTime();\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 if (state === 'visible') {\n this.updateLastActiveTime();\n }\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\nexport
|
|
1
|
+
{"version":3,"file":"session.cjs","names":[],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\nclass Session {\n private id: string;\n private startTime: number;\n private lastActiveTime: number;\n private accumulatedTime: number;\n\n private active: boolean;\n private visible: boolean;\n private focused: boolean;\n\n constructor() {\n this.id = uuidv7();\n this.startTime = Date.now();\n this.lastActiveTime = 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 getId = () => this.id;\n\n isActive = () => this.active;\n isVisible = () => this.visible;\n isFocused = () => this.focused;\n isExpired = () => Date.now() - this.lastActiveTime > SESSION_TIMEOUT;\n\n updateLastActiveTime = () => {\n this.lastActiveTime = Date.now();\n };\n\n updateActive = (active: boolean) => {\n this.active = active;\n };\n\n refresh = () => {\n this.id = uuidv7();\n this.lastActiveTime = Date.now();\n };\n\n updateAccumulator = () => {\n const now = Date.now();\n if (this.focused && this.visible && this.active) {\n const delta = now - this.startTime;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n this.accumulatedTime += delta;\n }\n }\n this.startTime = now;\n };\n\n focus = () => {\n this.updateAccumulator();\n this.updateLastActiveTime();\n this.focused = true;\n };\n\n blur = () => {\n this.updateAccumulator();\n this.focused = false;\n };\n\n pageshow = () => {\n this.updateAccumulator();\n this.updateLastActiveTime();\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 if (state === 'visible') {\n this.updateLastActiveTime();\n }\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 buys two things: the server bundle can be\n * evaluated, and `startTime` marks when the session actually began rather\n * than when the isolate happened to start.\n */\nexport function getSession() {\n return (session ??= new Session());\n}\n"],"mappings":";;;AAEA,MAAa,kBAAkB,OAAU;AAEzC,IAAM,UAAN,MAAc;CAUZ,cAAc;EAWA,KAAA,cAAA,KAAK;EAEF,KAAA,iBAAA,KAAK;EACJ,KAAA,kBAAA,KAAK;EACL,KAAA,kBAAA,KAAK;EACL,KAAA,kBAAA,KAAK,IAAI,IAAI,KAAK,iBAAiB;EAExB,KAAA,6BAAA;GAC3B,KAAK,iBAAiB,KAAK,IAAI;EACjC;EAEgB,KAAA,gBAAA,WAAoB;GAClC,KAAK,SAAS;EAChB;EAEgB,KAAA,gBAAA;GACd,KAAK,MAAA,GAAA,KAAA,GAAA,CAAY;GACjB,KAAK,iBAAiB,KAAK,IAAI;EACjC;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,YAAY;EACnB;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,KAAK,qBAAqB;GAC1B,KAAK,UAAU;EACjB;EAEa,KAAA,aAAA;GACX,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,qBAAqB;GAC1B,KAAK,SAAS;EAChB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEoB,KAAA,oBAAA,UAAmC;GACrD,KAAK,kBAAkB;GACvB,IAAI,UAAU,WACZ,KAAK,qBAAqB;GAE5B,KAAK,UAAU,UAAU;EAC3B;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,MAAM,iBAAiB,KAAK;GAC5B,KAAK,kBAAkB;GACvB,OAAO;EACT;EA5EE,KAAK,MAAA,GAAA,KAAA,GAAA,CAAY;EACjB,KAAK,YAAY,KAAK,IAAI;EAC1B,KAAK,iBAAiB,KAAK,IAAI;EAC/B,KAAK,kBAAkB;EAEvB,KAAK,SAAS;EACd,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,oBAAoB,YAAY;EAC1F,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,SAAS,IAAI;CACzE;AAqEF;AAEA,IAAI;;;;;;;;;;;;;;;;;;;;;;;;AAyBJ,SAAgB,aAAa;CAC3B,OAAQ,YAAY,IAAI,QAAQ;AAClC"}
|
package/dist/setup/session.d.cts
CHANGED
|
@@ -25,7 +25,30 @@ declare class Session {
|
|
|
25
25
|
visibilitychange: (state: DocumentVisibilityState) => void;
|
|
26
26
|
flush: () => number;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The session, built the first time something asks for it.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately not a module-scope `new Session()`. The constructor calls
|
|
32
|
+
* `uuidv7()`, `uuid` draws its bytes from `crypto.getRandomValues`, and
|
|
33
|
+
* Cloudflare Workers reject that outside a request handler:
|
|
34
|
+
*
|
|
35
|
+
* Disallowed operation called within global scope. Asynchronous I/O
|
|
36
|
+
* (ex: fetch() or connect()), setting a timeout, and generating random
|
|
37
|
+
* values are not allowed within global scope.
|
|
38
|
+
*
|
|
39
|
+
* Module scope in a Worker is evaluated once when the isolate boots and is
|
|
40
|
+
* then shared by every request that isolate serves, so a random value drawn
|
|
41
|
+
* there would be the same for all of them — which is why the runtime refuses
|
|
42
|
+
* to produce one. A host that server-renders on Workers reaches this module
|
|
43
|
+
* through `track()` on the server as well, and the throw happened as the
|
|
44
|
+
* isolate booted, taking down every route before a component rendered.
|
|
45
|
+
*
|
|
46
|
+
* Everything here is per-visitor browser or app state, so deferring the
|
|
47
|
+
* construction costs nothing and buys two things: the server bundle can be
|
|
48
|
+
* evaluated, and `startTime` marks when the session actually began rather
|
|
49
|
+
* than when the isolate happened to start.
|
|
50
|
+
*/
|
|
51
|
+
declare function getSession(): Session;
|
|
29
52
|
//#endregion
|
|
30
|
-
export { SESSION_TIMEOUT,
|
|
53
|
+
export { SESSION_TIMEOUT, getSession };
|
|
31
54
|
//# sourceMappingURL=session.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.cts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAEa;cAEP;UACI;UACA;UACA;UACA;UAEA;UACA;UACA;EAER;EAWA;EAEA;EACA;EACA;EACA;EAEA;EAIA,eAAY;EAIZ;EAKA;EAWA;EAMA;EAKA;EAMA;EAKA,mBAAgB,OAAW;EAQ3B
|
|
1
|
+
{"version":3,"file":"session.d.cts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAEa;cAEP;UACI;UACA;UACA;UACA;UAEA;UACA;UACA;EAER;EAWA;EAEA;EACA;EACA;EACA;EAEA;EAIA,eAAY;EAIZ;EAKA;EAWA;EAMA;EAKA;EAMA;EAKA,mBAAgB,OAAW;EAQ3B;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCc,cAAU"}
|
package/dist/setup/session.d.mts
CHANGED
|
@@ -25,7 +25,30 @@ declare class Session {
|
|
|
25
25
|
visibilitychange: (state: DocumentVisibilityState) => void;
|
|
26
26
|
flush: () => number;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The session, built the first time something asks for it.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately not a module-scope `new Session()`. The constructor calls
|
|
32
|
+
* `uuidv7()`, `uuid` draws its bytes from `crypto.getRandomValues`, and
|
|
33
|
+
* Cloudflare Workers reject that outside a request handler:
|
|
34
|
+
*
|
|
35
|
+
* Disallowed operation called within global scope. Asynchronous I/O
|
|
36
|
+
* (ex: fetch() or connect()), setting a timeout, and generating random
|
|
37
|
+
* values are not allowed within global scope.
|
|
38
|
+
*
|
|
39
|
+
* Module scope in a Worker is evaluated once when the isolate boots and is
|
|
40
|
+
* then shared by every request that isolate serves, so a random value drawn
|
|
41
|
+
* there would be the same for all of them — which is why the runtime refuses
|
|
42
|
+
* to produce one. A host that server-renders on Workers reaches this module
|
|
43
|
+
* through `track()` on the server as well, and the throw happened as the
|
|
44
|
+
* isolate booted, taking down every route before a component rendered.
|
|
45
|
+
*
|
|
46
|
+
* Everything here is per-visitor browser or app state, so deferring the
|
|
47
|
+
* construction costs nothing and buys two things: the server bundle can be
|
|
48
|
+
* evaluated, and `startTime` marks when the session actually began rather
|
|
49
|
+
* than when the isolate happened to start.
|
|
50
|
+
*/
|
|
51
|
+
declare function getSession(): Session;
|
|
29
52
|
//#endregion
|
|
30
|
-
export { SESSION_TIMEOUT,
|
|
53
|
+
export { SESSION_TIMEOUT, getSession };
|
|
31
54
|
//# sourceMappingURL=session.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.mts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAEa;cAEP;UACI;UACA;UACA;UACA;UAEA;UACA;UACA;EAER;EAWA;EAEA;EACA;EACA;EACA;EAEA;EAIA,eAAY;EAIZ;EAKA;EAWA;EAMA;EAKA;EAMA;EAKA,mBAAgB,OAAW;EAQ3B
|
|
1
|
+
{"version":3,"file":"session.d.mts","names":[],"sources":["../../src/setup/session.ts"],"mappings":";cAEa;cAEP;UACI;UACA;UACA;UACA;UAEA;UACA;UACA;EAER;EAWA;EAEA;EACA;EACA;EACA;EAEA;EAIA,eAAY;EAIZ;EAKA;EAWA;EAMA;EAKA;EAMA;EAKA,mBAAgB,OAAW;EAQ3B;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCc,cAAU"}
|
package/dist/setup/session.mjs
CHANGED
|
@@ -64,8 +64,34 @@ var Session = class {
|
|
|
64
64
|
this.focused = typeof document !== "undefined" ? document.hasFocus() : true;
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
-
|
|
67
|
+
let session;
|
|
68
|
+
/**
|
|
69
|
+
* The session, built the first time something asks for it.
|
|
70
|
+
*
|
|
71
|
+
* Deliberately not a module-scope `new Session()`. The constructor calls
|
|
72
|
+
* `uuidv7()`, `uuid` draws its bytes from `crypto.getRandomValues`, and
|
|
73
|
+
* Cloudflare Workers reject that outside a request handler:
|
|
74
|
+
*
|
|
75
|
+
* Disallowed operation called within global scope. Asynchronous I/O
|
|
76
|
+
* (ex: fetch() or connect()), setting a timeout, and generating random
|
|
77
|
+
* values are not allowed within global scope.
|
|
78
|
+
*
|
|
79
|
+
* Module scope in a Worker is evaluated once when the isolate boots and is
|
|
80
|
+
* then shared by every request that isolate serves, so a random value drawn
|
|
81
|
+
* there would be the same for all of them — which is why the runtime refuses
|
|
82
|
+
* to produce one. A host that server-renders on Workers reaches this module
|
|
83
|
+
* through `track()` on the server as well, and the throw happened as the
|
|
84
|
+
* isolate booted, taking down every route before a component rendered.
|
|
85
|
+
*
|
|
86
|
+
* Everything here is per-visitor browser or app state, so deferring the
|
|
87
|
+
* construction costs nothing and buys two things: the server bundle can be
|
|
88
|
+
* evaluated, and `startTime` marks when the session actually began rather
|
|
89
|
+
* than when the isolate happened to start.
|
|
90
|
+
*/
|
|
91
|
+
function getSession() {
|
|
92
|
+
return session ??= new Session();
|
|
93
|
+
}
|
|
68
94
|
//#endregion
|
|
69
|
-
export { SESSION_TIMEOUT,
|
|
95
|
+
export { SESSION_TIMEOUT, getSession };
|
|
70
96
|
|
|
71
97
|
//# sourceMappingURL=session.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\nclass Session {\n private id: string;\n private startTime: number;\n private lastActiveTime: number;\n private accumulatedTime: number;\n\n private active: boolean;\n private visible: boolean;\n private focused: boolean;\n\n constructor() {\n this.id = uuidv7();\n this.startTime = Date.now();\n this.lastActiveTime = 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 getId = () => this.id;\n\n isActive = () => this.active;\n isVisible = () => this.visible;\n isFocused = () => this.focused;\n isExpired = () => Date.now() - this.lastActiveTime > SESSION_TIMEOUT;\n\n updateLastActiveTime = () => {\n this.lastActiveTime = Date.now();\n };\n\n updateActive = (active: boolean) => {\n this.active = active;\n };\n\n refresh = () => {\n this.id = uuidv7();\n this.lastActiveTime = Date.now();\n };\n\n updateAccumulator = () => {\n const now = Date.now();\n if (this.focused && this.visible && this.active) {\n const delta = now - this.startTime;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n this.accumulatedTime += delta;\n }\n }\n this.startTime = now;\n };\n\n focus = () => {\n this.updateAccumulator();\n this.updateLastActiveTime();\n this.focused = true;\n };\n\n blur = () => {\n this.updateAccumulator();\n this.focused = false;\n };\n\n pageshow = () => {\n this.updateAccumulator();\n this.updateLastActiveTime();\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 if (state === 'visible') {\n this.updateLastActiveTime();\n }\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\nexport
|
|
1
|
+
{"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/setup/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from 'uuid';\n\nexport const SESSION_TIMEOUT = 30 * 60 * 1000;\n\nclass Session {\n private id: string;\n private startTime: number;\n private lastActiveTime: number;\n private accumulatedTime: number;\n\n private active: boolean;\n private visible: boolean;\n private focused: boolean;\n\n constructor() {\n this.id = uuidv7();\n this.startTime = Date.now();\n this.lastActiveTime = 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 getId = () => this.id;\n\n isActive = () => this.active;\n isVisible = () => this.visible;\n isFocused = () => this.focused;\n isExpired = () => Date.now() - this.lastActiveTime > SESSION_TIMEOUT;\n\n updateLastActiveTime = () => {\n this.lastActiveTime = Date.now();\n };\n\n updateActive = (active: boolean) => {\n this.active = active;\n };\n\n refresh = () => {\n this.id = uuidv7();\n this.lastActiveTime = Date.now();\n };\n\n updateAccumulator = () => {\n const now = Date.now();\n if (this.focused && this.visible && this.active) {\n const delta = now - this.startTime;\n if (delta > 0 && delta < SESSION_TIMEOUT) {\n this.accumulatedTime += delta;\n }\n }\n this.startTime = now;\n };\n\n focus = () => {\n this.updateAccumulator();\n this.updateLastActiveTime();\n this.focused = true;\n };\n\n blur = () => {\n this.updateAccumulator();\n this.focused = false;\n };\n\n pageshow = () => {\n this.updateAccumulator();\n this.updateLastActiveTime();\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 if (state === 'visible') {\n this.updateLastActiveTime();\n }\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 buys two things: the server bundle can be\n * evaluated, and `startTime` marks when the session actually began rather\n * than when the isolate happened to start.\n */\nexport function getSession() {\n return (session ??= new Session());\n}\n"],"mappings":";;AAEA,MAAa,kBAAkB,OAAU;AAEzC,IAAM,UAAN,MAAc;CAUZ,cAAc;EAWA,KAAA,cAAA,KAAK;EAEF,KAAA,iBAAA,KAAK;EACJ,KAAA,kBAAA,KAAK;EACL,KAAA,kBAAA,KAAK;EACL,KAAA,kBAAA,KAAK,IAAI,IAAI,KAAK,iBAAiB;EAExB,KAAA,6BAAA;GAC3B,KAAK,iBAAiB,KAAK,IAAI;EACjC;EAEgB,KAAA,gBAAA,WAAoB;GAClC,KAAK,SAAS;EAChB;EAEgB,KAAA,gBAAA;GACd,KAAK,KAAKA,GAAO;GACjB,KAAK,iBAAiB,KAAK,IAAI;EACjC;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,YAAY;EACnB;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,KAAK,qBAAqB;GAC1B,KAAK,UAAU;EACjB;EAEa,KAAA,aAAA;GACX,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,qBAAqB;GAC1B,KAAK,SAAS;EAChB;EAEiB,KAAA,iBAAA;GACf,KAAK,kBAAkB;GACvB,KAAK,SAAS;EAChB;EAEoB,KAAA,oBAAA,UAAmC;GACrD,KAAK,kBAAkB;GACvB,IAAI,UAAU,WACZ,KAAK,qBAAqB;GAE5B,KAAK,UAAU,UAAU;EAC3B;EAEc,KAAA,cAAA;GACZ,KAAK,kBAAkB;GACvB,MAAM,iBAAiB,KAAK;GAC5B,KAAK,kBAAkB;GACvB,OAAO;EACT;EA5EE,KAAK,KAAKA,GAAO;EACjB,KAAK,YAAY,KAAK,IAAI;EAC1B,KAAK,iBAAiB,KAAK,IAAI;EAC/B,KAAK,kBAAkB;EAEvB,KAAK,SAAS;EACd,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,oBAAoB,YAAY;EAC1F,KAAK,UAAU,OAAO,aAAa,cAAc,SAAS,SAAS,IAAI;CACzE;AAqEF;AAEA,IAAI;;;;;;;;;;;;;;;;;;;;;;;;AAyBJ,SAAgB,aAAa;CAC3B,OAAQ,YAAY,IAAI,QAAQ;AAClC"}
|
package/dist/track/index.cjs
CHANGED
|
@@ -6,24 +6,34 @@ const require_visitor_index = require("../visitor/index.cjs");
|
|
|
6
6
|
let _shware_utils = require("@shware/utils");
|
|
7
7
|
//#region src/track/index.ts
|
|
8
8
|
const defaultOptions = { enableThirdPartyTracking: true };
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
let tokenBucket;
|
|
10
|
+
/**
|
|
11
|
+
* The rate limiter, built on the first send rather than at module scope: its
|
|
12
|
+
* constructor starts a refill `setInterval`, and Cloudflare Workers refuse to
|
|
13
|
+
* set a timer outside a request handler for the same reason they refuse to
|
|
14
|
+
* generate random values there — see `setup/session.ts`.
|
|
15
|
+
*/
|
|
16
|
+
function getTokenBucket() {
|
|
17
|
+
return tokenBucket ??= new _shware_utils.TokenBucket({
|
|
18
|
+
rate: 1,
|
|
19
|
+
capacity: 20,
|
|
20
|
+
requested: 2
|
|
21
|
+
});
|
|
22
|
+
}
|
|
14
23
|
async function sendEvents(events) {
|
|
15
24
|
try {
|
|
16
25
|
if (events.length === 0) return;
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
const session = require_setup_session.getSession();
|
|
27
|
+
if (session.isExpired()) {
|
|
28
|
+
session.refresh();
|
|
19
29
|
events.unshift({
|
|
20
30
|
name: "session_start",
|
|
21
31
|
properties: {},
|
|
22
32
|
options: { enableThirdPartyTracking: false },
|
|
23
33
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
24
34
|
});
|
|
25
|
-
} else
|
|
26
|
-
await
|
|
35
|
+
} else session.updateLastActiveTime();
|
|
36
|
+
await getTokenBucket().removeTokens();
|
|
27
37
|
const tags = await require_setup_index.config.getTags();
|
|
28
38
|
const visitor_id = (await require_visitor_index.getVisitor()).id;
|
|
29
39
|
const dto = events.map((event) => ({
|
|
@@ -31,7 +41,7 @@ async function sendEvents(events) {
|
|
|
31
41
|
properties: event.properties,
|
|
32
42
|
tags,
|
|
33
43
|
visitor_id,
|
|
34
|
-
session_id:
|
|
44
|
+
session_id: session.getId(),
|
|
35
45
|
platform: require_setup_index.config.platform,
|
|
36
46
|
environment: require_setup_index.config.environment,
|
|
37
47
|
timestamp: event.timestamp
|
|
@@ -98,13 +108,14 @@ async function trackAsync(name, properties, options = defaultOptions) {
|
|
|
98
108
|
}
|
|
99
109
|
function sendBeacon(name, properties) {
|
|
100
110
|
if (!require_setup_index.cache.tags || !require_setup_index.cache.visitor) return;
|
|
101
|
-
require_setup_session.
|
|
111
|
+
const session = require_setup_session.getSession();
|
|
112
|
+
session.updateLastActiveTime();
|
|
102
113
|
const dto = [{
|
|
103
114
|
name,
|
|
104
115
|
properties,
|
|
105
116
|
tags: require_setup_index.cache.tags,
|
|
106
117
|
visitor_id: require_setup_index.cache.visitor.id,
|
|
107
|
-
session_id:
|
|
118
|
+
session_id: session.getId(),
|
|
108
119
|
platform: require_setup_index.config.platform,
|
|
109
120
|
environment: require_setup_index.config.environment,
|
|
110
121
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
package/dist/track/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["TokenBucket","
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["TokenBucket","getSession","config","getVisitor","IGNORED_EVENTS","cache"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties } 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 timestamp: string;\n options: TrackOptions;\n};\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n const session = getSession();\n if (session.isExpired()) {\n session.refresh();\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n timestamp: new Date().toISOString(),\n });\n } else {\n session.updateLastActiveTime();\n }\n\n await getTokenBucket().removeTokens();\n\n const tags = await config.getTags();\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = events.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags,\n visitor_id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data[index].id;\n options.onSucceed?.({ id: eventId });\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({ name, properties, options, timestamp: new Date().toISOString() });\n if (list.length >= batch) {\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n }, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([{ name, properties, options, timestamp: new Date().toISOString() }]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n if (!cache.tags || !cache.visitor) return;\n\n const session = getSession();\n session.updateLastActiveTime();\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n tags: cache.tags,\n visitor_id: cache.visitor.id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAmGG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAkBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAKxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAmGG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAkBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAKxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
|
package/dist/track/index.mjs
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { cache, config } from "../setup/index.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { getSession } from "../setup/session.mjs";
|
|
3
3
|
import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
|
|
4
4
|
import { getVisitor } from "../visitor/index.mjs";
|
|
5
5
|
import { TokenBucket, fetch } from "@shware/utils";
|
|
6
6
|
//#region src/track/index.ts
|
|
7
7
|
const defaultOptions = { enableThirdPartyTracking: true };
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
let tokenBucket;
|
|
9
|
+
/**
|
|
10
|
+
* The rate limiter, built on the first send rather than at module scope: its
|
|
11
|
+
* constructor starts a refill `setInterval`, and Cloudflare Workers refuse to
|
|
12
|
+
* set a timer outside a request handler for the same reason they refuse to
|
|
13
|
+
* generate random values there — see `setup/session.ts`.
|
|
14
|
+
*/
|
|
15
|
+
function getTokenBucket() {
|
|
16
|
+
return tokenBucket ??= new TokenBucket({
|
|
17
|
+
rate: 1,
|
|
18
|
+
capacity: 20,
|
|
19
|
+
requested: 2
|
|
20
|
+
});
|
|
21
|
+
}
|
|
13
22
|
async function sendEvents(events) {
|
|
14
23
|
try {
|
|
15
24
|
if (events.length === 0) return;
|
|
25
|
+
const session = getSession();
|
|
16
26
|
if (session.isExpired()) {
|
|
17
27
|
session.refresh();
|
|
18
28
|
events.unshift({
|
|
@@ -22,7 +32,7 @@ async function sendEvents(events) {
|
|
|
22
32
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
23
33
|
});
|
|
24
34
|
} else session.updateLastActiveTime();
|
|
25
|
-
await
|
|
35
|
+
await getTokenBucket().removeTokens();
|
|
26
36
|
const tags = await config.getTags();
|
|
27
37
|
const visitor_id = (await getVisitor()).id;
|
|
28
38
|
const dto = events.map((event) => ({
|
|
@@ -97,6 +107,7 @@ async function trackAsync(name, properties, options = defaultOptions) {
|
|
|
97
107
|
}
|
|
98
108
|
function sendBeacon(name, properties) {
|
|
99
109
|
if (!cache.tags || !cache.visitor) return;
|
|
110
|
+
const session = getSession();
|
|
100
111
|
session.updateLastActiveTime();
|
|
101
112
|
const dto = [{
|
|
102
113
|
name,
|
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 type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport {
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties } 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 timestamp: string;\n options: TrackOptions;\n};\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n const session = getSession();\n if (session.isExpired()) {\n session.refresh();\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n timestamp: new Date().toISOString(),\n });\n } else {\n session.updateLastActiveTime();\n }\n\n await getTokenBucket().removeTokens();\n\n const tags = await config.getTags();\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = events.map((event) => ({\n name: event.name,\n properties: event.properties,\n tags,\n visitor_id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }));\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data[index].id;\n options.onSucceed?.({ id: eventId });\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({ name, properties, options, timestamp: new Date().toISOString() });\n if (list.length >= batch) {\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n }, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([{ name, properties, options, timestamp: new Date().toISOString() }]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n if (!cache.tags || !cache.visitor) return;\n\n const session = getSession();\n session.updateLastActiveTime();\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n tags: cache.tags,\n visitor_id: cache.visitor.id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|