@shware/analytics 6.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/use-web-analytics.cjs +1 -1
- package/dist/hooks/use-web-analytics.cjs.map +1 -1
- package/dist/hooks/use-web-analytics.mjs +1 -1
- package/dist/hooks/use-web-analytics.mjs.map +1 -1
- package/dist/native/setup.cjs +24 -2
- package/dist/native/setup.cjs.map +1 -1
- package/dist/native/setup.d.cts.map +1 -1
- package/dist/native/setup.d.mts.map +1 -1
- package/dist/native/setup.mjs +24 -2
- package/dist/native/setup.mjs.map +1 -1
- package/dist/schema/index.cjs +2 -1
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts +8 -4
- package/dist/schema/index.d.cts.map +1 -1
- package/dist/schema/index.d.mts +8 -4
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/schema/index.mjs +2 -1
- package/dist/schema/index.mjs.map +1 -1
- package/dist/server/linkedin-conversions-api.cjs +1 -1
- package/dist/server/linkedin-conversions-api.cjs.map +1 -1
- package/dist/server/linkedin-conversions-api.mjs +1 -1
- package/dist/server/linkedin-conversions-api.mjs.map +1 -1
- package/dist/server/meta-conversions-api.cjs +7 -8
- package/dist/server/meta-conversions-api.cjs.map +1 -1
- package/dist/server/meta-conversions-api.d.cts.map +1 -1
- package/dist/server/meta-conversions-api.d.mts.map +1 -1
- package/dist/server/meta-conversions-api.mjs +7 -8
- package/dist/server/meta-conversions-api.mjs.map +1 -1
- package/dist/server/openai-conversions-api.cjs +2 -2
- package/dist/server/openai-conversions-api.cjs.map +1 -1
- package/dist/server/openai-conversions-api.mjs +2 -2
- package/dist/server/openai-conversions-api.mjs.map +1 -1
- package/dist/server/reddit-conversions-api.cjs +2 -2
- package/dist/server/reddit-conversions-api.cjs.map +1 -1
- package/dist/server/reddit-conversions-api.mjs +2 -2
- package/dist/server/reddit-conversions-api.mjs.map +1 -1
- package/dist/track/index.cjs +41 -15
- 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 +41 -15
- package/dist/track/index.mjs.map +1 -1
- package/dist/track/types.d.cts +13 -4
- package/dist/track/types.d.cts.map +1 -1
- package/dist/track/types.d.mts +13 -4
- package/dist/track/types.d.mts.map +1 -1
- package/dist/visitor/index.cjs +10 -5
- package/dist/visitor/index.cjs.map +1 -1
- package/dist/visitor/index.d.cts.map +1 -1
- package/dist/visitor/index.d.mts.map +1 -1
- package/dist/visitor/index.mjs +10 -5
- package/dist/visitor/index.mjs.map +1 -1
- package/dist/web/index.cjs +28 -6
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts.map +1 -1
- package/dist/web/index.d.mts.map +1 -1
- package/dist/web/index.mjs +28 -6
- package/dist/web/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/track/index.cjs
CHANGED
|
@@ -20,6 +20,23 @@ function getTokenBucket() {
|
|
|
20
20
|
requested: 2
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Tags belong to the moment the event happened, not to the moment its batch goes out: a queued
|
|
25
|
+
* event waits up to `delay` ms, and a single page app can navigate in that window, which would
|
|
26
|
+
* stamp every pending event with the URL of the page the user has already left.
|
|
27
|
+
*
|
|
28
|
+
* The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed
|
|
29
|
+
* here — an unhandled rejection would surface as a global error long before `sendEvents` could
|
|
30
|
+
* catch it. Falling back to the last built tags keeps the event, minus whatever changed since.
|
|
31
|
+
*/
|
|
32
|
+
async function captureTags() {
|
|
33
|
+
try {
|
|
34
|
+
return await require_setup_index.config.getTags();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (e instanceof Error) console.log(e.message);
|
|
37
|
+
return require_setup_index.cache.tags ?? {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
23
40
|
async function sendEvents(events) {
|
|
24
41
|
try {
|
|
25
42
|
if (events.length === 0) return;
|
|
@@ -30,22 +47,22 @@ async function sendEvents(events) {
|
|
|
30
47
|
name: "session_start",
|
|
31
48
|
properties: {},
|
|
32
49
|
options: { enableThirdPartyTracking: false },
|
|
50
|
+
tags: captureTags(),
|
|
33
51
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
34
52
|
});
|
|
35
53
|
} else session.updateLastActiveTime();
|
|
36
54
|
await getTokenBucket().removeTokens();
|
|
37
|
-
const tags = await require_setup_index.config.getTags();
|
|
38
55
|
const visitor_id = (await require_visitor_index.getVisitor()).id;
|
|
39
|
-
const dto = events.map((event) => ({
|
|
56
|
+
const dto = await Promise.all(events.map(async (event) => ({
|
|
40
57
|
name: event.name,
|
|
41
58
|
properties: event.properties,
|
|
42
|
-
tags,
|
|
59
|
+
tags: await event.tags,
|
|
43
60
|
visitor_id,
|
|
44
61
|
session_id: session.getId(),
|
|
45
62
|
platform: require_setup_index.config.platform,
|
|
46
63
|
environment: require_setup_index.config.environment,
|
|
47
64
|
timestamp: event.timestamp
|
|
48
|
-
}));
|
|
65
|
+
})));
|
|
49
66
|
const response = await (0, _shware_utils.fetch)(`${require_setup_index.config.endpoint}/events`, {
|
|
50
67
|
method: "POST",
|
|
51
68
|
credentials: "include",
|
|
@@ -62,8 +79,8 @@ async function sendEvents(events) {
|
|
|
62
79
|
continue;
|
|
63
80
|
}
|
|
64
81
|
const { options, name, properties } = event;
|
|
65
|
-
const eventId = data
|
|
66
|
-
options.onSucceed?.({ id: eventId });
|
|
82
|
+
const eventId = data.at(index)?.id;
|
|
83
|
+
options.onSucceed?.(eventId ? { id: eventId } : void 0);
|
|
67
84
|
index++;
|
|
68
85
|
if (!options.enableThirdPartyTracking || require_third_parties_ignored_events.IGNORED_EVENTS.includes(name)) continue;
|
|
69
86
|
require_setup_index.config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));
|
|
@@ -77,32 +94,41 @@ const batch = 10;
|
|
|
77
94
|
const delay = 2e3;
|
|
78
95
|
const list = [];
|
|
79
96
|
let timer = null;
|
|
97
|
+
/**
|
|
98
|
+
* Both paths into a send go through here, so a batch that fills up cancels the timer the
|
|
99
|
+
* previous push armed rather than leaving it to wake up on its own with nothing to send.
|
|
100
|
+
*/
|
|
101
|
+
function flush() {
|
|
102
|
+
if (timer) {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
timer = null;
|
|
105
|
+
}
|
|
106
|
+
if (list.length === 0) return;
|
|
107
|
+
const copy = [...list];
|
|
108
|
+
list.length = 0;
|
|
109
|
+
sendEvents(copy);
|
|
110
|
+
}
|
|
80
111
|
function track(name, properties, options = defaultOptions) {
|
|
81
112
|
list.push({
|
|
82
113
|
name,
|
|
83
114
|
properties,
|
|
84
115
|
options,
|
|
116
|
+
tags: captureTags(),
|
|
85
117
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
86
118
|
});
|
|
87
119
|
if (list.length >= batch) {
|
|
88
|
-
|
|
89
|
-
list.length = 0;
|
|
90
|
-
sendEvents(copy);
|
|
120
|
+
flush();
|
|
91
121
|
return;
|
|
92
122
|
}
|
|
93
123
|
if (timer) clearTimeout(timer);
|
|
94
|
-
timer = setTimeout(
|
|
95
|
-
timer = null;
|
|
96
|
-
const copy = [...list];
|
|
97
|
-
list.length = 0;
|
|
98
|
-
sendEvents(copy);
|
|
99
|
-
}, delay);
|
|
124
|
+
timer = setTimeout(flush, delay);
|
|
100
125
|
}
|
|
101
126
|
async function trackAsync(name, properties, options = defaultOptions) {
|
|
102
127
|
await sendEvents([{
|
|
103
128
|
name,
|
|
104
129
|
properties,
|
|
105
130
|
options,
|
|
131
|
+
tags: captureTags(),
|
|
106
132
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
107
133
|
}]);
|
|
108
134
|
}
|
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","config","cache","getSession","getVisitor","IGNORED_EVENTS"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = { enableThirdPartyTracking: true };\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n const session = getSession();\n if (session.isExpired()) {\n session.refresh();\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n } else {\n session.updateLastActiveTime();\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n if (!cache.tags || !cache.visitor) return;\n\n const session = getSession();\n session.updateLastActiveTime();\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n tags: cache.tags,\n visitor_id: cache.visitor.id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAuIG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UAQiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAuIG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
|
package/dist/track/index.mjs
CHANGED
|
@@ -19,6 +19,23 @@ function getTokenBucket() {
|
|
|
19
19
|
requested: 2
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Tags belong to the moment the event happened, not to the moment its batch goes out: a queued
|
|
24
|
+
* event waits up to `delay` ms, and a single page app can navigate in that window, which would
|
|
25
|
+
* stamp every pending event with the URL of the page the user has already left.
|
|
26
|
+
*
|
|
27
|
+
* The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed
|
|
28
|
+
* here — an unhandled rejection would surface as a global error long before `sendEvents` could
|
|
29
|
+
* catch it. Falling back to the last built tags keeps the event, minus whatever changed since.
|
|
30
|
+
*/
|
|
31
|
+
async function captureTags() {
|
|
32
|
+
try {
|
|
33
|
+
return await config.getTags();
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (e instanceof Error) console.log(e.message);
|
|
36
|
+
return cache.tags ?? {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
22
39
|
async function sendEvents(events) {
|
|
23
40
|
try {
|
|
24
41
|
if (events.length === 0) return;
|
|
@@ -29,22 +46,22 @@ async function sendEvents(events) {
|
|
|
29
46
|
name: "session_start",
|
|
30
47
|
properties: {},
|
|
31
48
|
options: { enableThirdPartyTracking: false },
|
|
49
|
+
tags: captureTags(),
|
|
32
50
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
33
51
|
});
|
|
34
52
|
} else session.updateLastActiveTime();
|
|
35
53
|
await getTokenBucket().removeTokens();
|
|
36
|
-
const tags = await config.getTags();
|
|
37
54
|
const visitor_id = (await getVisitor()).id;
|
|
38
|
-
const dto = events.map((event) => ({
|
|
55
|
+
const dto = await Promise.all(events.map(async (event) => ({
|
|
39
56
|
name: event.name,
|
|
40
57
|
properties: event.properties,
|
|
41
|
-
tags,
|
|
58
|
+
tags: await event.tags,
|
|
42
59
|
visitor_id,
|
|
43
60
|
session_id: session.getId(),
|
|
44
61
|
platform: config.platform,
|
|
45
62
|
environment: config.environment,
|
|
46
63
|
timestamp: event.timestamp
|
|
47
|
-
}));
|
|
64
|
+
})));
|
|
48
65
|
const response = await fetch(`${config.endpoint}/events`, {
|
|
49
66
|
method: "POST",
|
|
50
67
|
credentials: "include",
|
|
@@ -61,8 +78,8 @@ async function sendEvents(events) {
|
|
|
61
78
|
continue;
|
|
62
79
|
}
|
|
63
80
|
const { options, name, properties } = event;
|
|
64
|
-
const eventId = data
|
|
65
|
-
options.onSucceed?.({ id: eventId });
|
|
81
|
+
const eventId = data.at(index)?.id;
|
|
82
|
+
options.onSucceed?.(eventId ? { id: eventId } : void 0);
|
|
66
83
|
index++;
|
|
67
84
|
if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) continue;
|
|
68
85
|
config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));
|
|
@@ -76,32 +93,41 @@ const batch = 10;
|
|
|
76
93
|
const delay = 2e3;
|
|
77
94
|
const list = [];
|
|
78
95
|
let timer = null;
|
|
96
|
+
/**
|
|
97
|
+
* Both paths into a send go through here, so a batch that fills up cancels the timer the
|
|
98
|
+
* previous push armed rather than leaving it to wake up on its own with nothing to send.
|
|
99
|
+
*/
|
|
100
|
+
function flush() {
|
|
101
|
+
if (timer) {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
timer = null;
|
|
104
|
+
}
|
|
105
|
+
if (list.length === 0) return;
|
|
106
|
+
const copy = [...list];
|
|
107
|
+
list.length = 0;
|
|
108
|
+
sendEvents(copy);
|
|
109
|
+
}
|
|
79
110
|
function track(name, properties, options = defaultOptions) {
|
|
80
111
|
list.push({
|
|
81
112
|
name,
|
|
82
113
|
properties,
|
|
83
114
|
options,
|
|
115
|
+
tags: captureTags(),
|
|
84
116
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
85
117
|
});
|
|
86
118
|
if (list.length >= batch) {
|
|
87
|
-
|
|
88
|
-
list.length = 0;
|
|
89
|
-
sendEvents(copy);
|
|
119
|
+
flush();
|
|
90
120
|
return;
|
|
91
121
|
}
|
|
92
122
|
if (timer) clearTimeout(timer);
|
|
93
|
-
timer = setTimeout(
|
|
94
|
-
timer = null;
|
|
95
|
-
const copy = [...list];
|
|
96
|
-
list.length = 0;
|
|
97
|
-
sendEvents(copy);
|
|
98
|
-
}, delay);
|
|
123
|
+
timer = setTimeout(flush, delay);
|
|
99
124
|
}
|
|
100
125
|
async function trackAsync(name, properties, options = defaultOptions) {
|
|
101
126
|
await sendEvents([{
|
|
102
127
|
name,
|
|
103
128
|
properties,
|
|
104
129
|
options,
|
|
130
|
+
tags: captureTags(),
|
|
105
131
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
106
132
|
}]);
|
|
107
133
|
}
|
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 { 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
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = { enableThirdPartyTracking: true };\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n const session = getSession();\n if (session.isExpired()) {\n session.refresh();\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n } else {\n session.updateLastActiveTime();\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n if (!options.enableThirdPartyTracking || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => tracker(name, properties, eventId));\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n if (!cache.tags || !cache.visitor) return;\n\n const session = getSession();\n session.updateLastActiveTime();\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n tags: cache.tags,\n visitor_id: cache.visitor.id,\n session_id: session.getId(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
|
package/dist/track/types.d.cts
CHANGED
|
@@ -62,9 +62,18 @@ interface EnvironmentInfo {
|
|
|
62
62
|
language?: string;
|
|
63
63
|
time_zone?: string;
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Page context, captured when the event happens rather than when its batch is sent, and named
|
|
67
|
+
* after the GA4 parameters it mirrors — gtag sends `page_location`, `page_referrer` and
|
|
68
|
+
* `page_title` with every event, not just with `page_view`.
|
|
69
|
+
*
|
|
70
|
+
* Web only: a native app has no page. Meta's `event_source_url` and OpenAI's `source_url` are
|
|
71
|
+
* both fed from `page_location`.
|
|
72
|
+
*/
|
|
73
|
+
interface PageInfo {
|
|
74
|
+
page_location?: string;
|
|
67
75
|
page_referrer?: string;
|
|
76
|
+
page_title?: string;
|
|
68
77
|
}
|
|
69
78
|
interface AdvertisingInfo {
|
|
70
79
|
/**
|
|
@@ -142,7 +151,7 @@ interface UTMParams {
|
|
|
142
151
|
/** Targeting criteria applied to the campaign */
|
|
143
152
|
utm_marketing_tactic?: 'remarketing' | 'prospecting' | (string & {});
|
|
144
153
|
}
|
|
145
|
-
interface TrackTags extends PlatformInfo, DeviceInfo, AppInfo, EnvironmentInfo,
|
|
154
|
+
interface TrackTags extends PlatformInfo, DeviceInfo, AppInfo, EnvironmentInfo, PageInfo, AdvertisingInfo, UTMParams {
|
|
146
155
|
idempotency_key?: string;
|
|
147
156
|
[key: string]: string | number | boolean | null | undefined;
|
|
148
157
|
}
|
|
@@ -162,5 +171,5 @@ type TrackEventResponse = {
|
|
|
162
171
|
id: string;
|
|
163
172
|
}[];
|
|
164
173
|
//#endregion
|
|
165
|
-
export { AdvertisingInfo, AllowedPropertyValues, AppInfo, DeviceInfo, Environment, EnvironmentInfo, EventName, Platform, PlatformInfo,
|
|
174
|
+
export { AdvertisingInfo, AllowedPropertyValues, AppInfo, DeviceInfo, Environment, EnvironmentInfo, EventName, PageInfo, Platform, PlatformInfo, ThirdPartyTracker, TrackEvent, TrackEventResponse, TrackName, TrackProperties, TrackTags, UTMParams, UserData, UserProvidedData };
|
|
166
175
|
//# sourceMappingURL=types.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.cts","names":[],"sources":["../../src/track/types.ts"],"mappings":";;KAEY;KACA,YAAY;KAEZ,UAAU,UAAU,YAAY,aAAa,gBAAgB,iBACrE,IACA;KACQ,gBAAgB,UAAU,YAAY,aAAa,gBAAgB,iBAC3E,eAAe,KACf,OAAO,mBAAmB;KAElB;KACA;UAEK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe,yBAAyB;EACxC;EACA;EACA;EACA;EACA;IAAa;IAAc;IAAe;;EAE1C;EACA;;KAGU,qBAAqB,UAAU,WACzC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;;EAEf;EACA;;UAGe;EACf;EACA;EACA
|
|
1
|
+
{"version":3,"file":"types.d.cts","names":[],"sources":["../../src/track/types.ts"],"mappings":";;KAEY;KACA,YAAY;KAEZ,UAAU,UAAU,YAAY,aAAa,gBAAgB,iBACrE,IACA;KACQ,gBAAgB,UAAU,YAAY,aAAa,gBAAgB,iBAC3E,eAAe,KACf,OAAO,mBAAmB;KAElB;KACA;UAEK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe,yBAAyB;EACxC;EACA;EACA;EACA;EACA;IAAa;IAAc;IAAe;;EAE1C;EACA;;KAGU,qBAAqB,UAAU,WACzC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;;EAEf;EACA;;UAGe;EACf;EACA;EACA;;;;;;;;;;UAWe;EACf;EACA;EACA;;UAGe;;;;;;;EAOf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;;EAEA;EACA;;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;UAQe;;EAEf;;;;;;;;;;;;EAqCA;;;;;;EAuDA;;EAMA;;EAEA;;EAEA;;EAEA;;EAUA;;EAEA;;UAGe,kBACP,cAAc,YAAY,SAAS,iBAAiB,UAAU,iBAAiB;EACvF;GACC;;UAGc,WAAW,UAAU,YAAY;EAChD;EACA,MAAM,UAAU;EAChB,MAAM;EACN;EACA;EACA,UAAU;EACV,aAAa;EACb,aAAa,gBAAgB;EAC7B;;KAGU;;EAEV"}
|
package/dist/track/types.d.mts
CHANGED
|
@@ -62,9 +62,18 @@ interface EnvironmentInfo {
|
|
|
62
62
|
language?: string;
|
|
63
63
|
time_zone?: string;
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Page context, captured when the event happens rather than when its batch is sent, and named
|
|
67
|
+
* after the GA4 parameters it mirrors — gtag sends `page_location`, `page_referrer` and
|
|
68
|
+
* `page_title` with every event, not just with `page_view`.
|
|
69
|
+
*
|
|
70
|
+
* Web only: a native app has no page. Meta's `event_source_url` and OpenAI's `source_url` are
|
|
71
|
+
* both fed from `page_location`.
|
|
72
|
+
*/
|
|
73
|
+
interface PageInfo {
|
|
74
|
+
page_location?: string;
|
|
67
75
|
page_referrer?: string;
|
|
76
|
+
page_title?: string;
|
|
68
77
|
}
|
|
69
78
|
interface AdvertisingInfo {
|
|
70
79
|
/**
|
|
@@ -142,7 +151,7 @@ interface UTMParams {
|
|
|
142
151
|
/** Targeting criteria applied to the campaign */
|
|
143
152
|
utm_marketing_tactic?: 'remarketing' | 'prospecting' | (string & {});
|
|
144
153
|
}
|
|
145
|
-
interface TrackTags extends PlatformInfo, DeviceInfo, AppInfo, EnvironmentInfo,
|
|
154
|
+
interface TrackTags extends PlatformInfo, DeviceInfo, AppInfo, EnvironmentInfo, PageInfo, AdvertisingInfo, UTMParams {
|
|
146
155
|
idempotency_key?: string;
|
|
147
156
|
[key: string]: string | number | boolean | null | undefined;
|
|
148
157
|
}
|
|
@@ -162,5 +171,5 @@ type TrackEventResponse = {
|
|
|
162
171
|
id: string;
|
|
163
172
|
}[];
|
|
164
173
|
//#endregion
|
|
165
|
-
export { AdvertisingInfo, AllowedPropertyValues, AppInfo, DeviceInfo, Environment, EnvironmentInfo, EventName, Platform, PlatformInfo,
|
|
174
|
+
export { AdvertisingInfo, AllowedPropertyValues, AppInfo, DeviceInfo, Environment, EnvironmentInfo, EventName, PageInfo, Platform, PlatformInfo, ThirdPartyTracker, TrackEvent, TrackEventResponse, TrackName, TrackProperties, TrackTags, UTMParams, UserData, UserProvidedData };
|
|
166
175
|
//# sourceMappingURL=types.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../src/track/types.ts"],"mappings":";;KAEY;KACA,YAAY;KAEZ,UAAU,UAAU,YAAY,aAAa,gBAAgB,iBACrE,IACA;KACQ,gBAAgB,UAAU,YAAY,aAAa,gBAAgB,iBAC3E,eAAe,KACf,OAAO,mBAAmB;KAElB;KACA;UAEK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe,yBAAyB;EACxC;EACA;EACA;EACA;EACA;IAAa;IAAc;IAAe;;EAE1C;EACA;;KAGU,qBAAqB,UAAU,WACzC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;;EAEf;EACA;;UAGe;EACf;EACA;EACA
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../src/track/types.ts"],"mappings":";;KAEY;KACA,YAAY;KAEZ,UAAU,UAAU,YAAY,aAAa,gBAAgB,iBACrE,IACA;KACQ,gBAAgB,UAAU,YAAY,aAAa,gBAAgB,iBAC3E,eAAe,KACf,OAAO,mBAAmB;KAElB;KACA;UAEK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe,yBAAyB;EACxC;EACA;EACA;EACA;EACA;IAAa;IAAc;IAAe;;EAE1C;EACA;;KAGU,qBAAqB,UAAU,WACzC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;;EAEf;EACA;;UAGe;EACf;EACA;EACA;;;;;;;;;;UAWe;EACf;EACA;EACA;;UAGe;;;;;;;EAOf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;;EAEA;EACA;;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;UAQe;;EAEf;;;;;;;;;;;;EAqCA;;;;;;EAuDA;;EAMA;;EAEA;;EAEA;;EAEA;;EAUA;;EAEA;;UAGe,kBACP,cAAc,YAAY,SAAS,iBAAiB,UAAU,iBAAiB;EACvF;GACC;;UAGc,WAAW,UAAU,YAAY;EAChD;EACA,MAAM,UAAU;EAChB,MAAM;EACN;EACA;EACA,UAAU;EACV,aAAa;EACb,aAAa,gBAAgB;EAC7B;;KAGU;;EAEV"}
|
package/dist/visitor/index.cjs
CHANGED
|
@@ -12,12 +12,14 @@ async function createVisitor() {
|
|
|
12
12
|
tags,
|
|
13
13
|
properties: tags
|
|
14
14
|
};
|
|
15
|
-
const
|
|
15
|
+
const response = await (0, _shware_utils.fetch)(`${require_setup_index.config.endpoint}/visitors`, {
|
|
16
16
|
method: "POST",
|
|
17
17
|
credentials: "include",
|
|
18
18
|
headers: await require_setup_index.config.getHeaders(),
|
|
19
19
|
body: JSON.stringify(dto)
|
|
20
|
-
})
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) throw new Error(`Failed to create visitor: ${response.status} ${await response.text()}`);
|
|
22
|
+
const data = await response.json();
|
|
21
23
|
if (data.id) require_setup_index.config.storage.setItem(require_constants_storage.keys.visitor_id, data.id);
|
|
22
24
|
return data;
|
|
23
25
|
}
|
|
@@ -42,9 +44,12 @@ async function getVisitor() {
|
|
|
42
44
|
if (require_setup_index.cache.visitor) return require_setup_index.cache.visitor;
|
|
43
45
|
if (visitorFetcher) return visitorFetcher;
|
|
44
46
|
visitorFetcher = getOrCreateVisitor();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
try {
|
|
48
|
+
require_setup_index.cache.visitor = await visitorFetcher;
|
|
49
|
+
return require_setup_index.cache.visitor;
|
|
50
|
+
} finally {
|
|
51
|
+
visitorFetcher = null;
|
|
52
|
+
}
|
|
48
53
|
}
|
|
49
54
|
async function setVisitor(dto) {
|
|
50
55
|
const { id } = await getVisitor();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["config","keys","cache"],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n cache.visitor = await visitorFetcher;\n visitorFetcher = null;\n
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["config","keys","cache"],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create visitor: ${response.status} ${await response.text()}`);\n }\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n try {\n cache.visitor = await visitorFetcher;\n return cache.visitor;\n } finally {\n // In a `finally`, so a rejected attempt is not left in `visitorFetcher` for every later\n // caller to await again: `sendEvents` needs a visitor for every batch, and one failed\n // request would otherwise stop the page from reporting anything until it is reloaded.\n visitorFetcher = null;\n }\n}\n\nexport async function setVisitor(dto: Omit<UpdateVisitorDTO, 'tags'>) {\n const { id } = await getVisitor();\n const tags = await config.getTags();\n const body: UpdateVisitorDTO = { ...dto, tags };\n const response = await fetch(`${config.endpoint}/visitors/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) throw new Error('Failed to set visitor');\n const data = (await response.json()) as Visitor;\n\n config.thirdPartyUserSetters.forEach((setter) => setter(body));\n cache.visitor = data;\n return data;\n}\n"],"mappings":";;;;;AAMA,eAAe,gBAAkC;CAC/C,MAAM,OAAO,MAAMA,oBAAAA,OAAO,QAAQ;CAClC,MAAM,MAAwB;EAC5B,WAAW,MAAMA,oBAAAA,OAAO,YAAY;EACpC,UAAUA,oBAAAA,OAAO;EACjB,aAAaA,oBAAAA,OAAO;EACpB;EACA,YAAY;CACd;CAEA,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY;EAC1D,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC;CAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,6BAA6B,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;CAEzF,MAAM,OAAQ,MAAM,SAAS,KAAK;CAClC,IAAI,KAAK,IACP,oBAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,YAAY,KAAK,EAAE;CAEjD,OAAO;AACT;AAEA,eAAe,qBAAuC;CACpD,MAAM,YAAYD,oBAAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,UAAU;CACxD,IAAI,aAAa,cAAc,aAAa;EAS1C,MAAM,OAAO,MAAMD,oBAAAA,OAAO,QAAQ;EAClC,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY,aAAa;GACvE,QAAQ;GACR,aAAa;GACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,EAAE,KAAK,CAA4B;EAC1D,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,cAAc;EACvC,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,IACP,oBAAA,OAAO,QAAQ,QAAQC,0BAAAA,KAAK,YAAY,KAAK,EAAE;EAEjD,OAAO;CACT,OACE,OAAO,cAAc;AAEzB;AAEA,IAAI,iBAA0C;AAE9C,eAAsB,aAA+B;CACnD,IAAIC,oBAAAA,MAAM,SAAS,OAAOA,oBAAAA,MAAM;CAChC,IAAI,gBAAgB,OAAO;CAC3B,iBAAiB,mBAAmB;CACpC,IAAI;EACF,oBAAA,MAAM,UAAU,MAAM;EACtB,OAAOA,oBAAAA,MAAM;CACf,UAAU;EAIR,iBAAiB;CACnB;AACF;AAEA,eAAsB,WAAW,KAAqC;CACpE,MAAM,EAAE,OAAO,MAAM,WAAW;CAChC,MAAM,OAAO,MAAMF,oBAAAA,OAAO,QAAQ;CAClC,MAAM,OAAyB;EAAE,GAAG;EAAK;CAAK;CAC9C,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,GAAGA,oBAAAA,OAAO,SAAS,YAAY,MAAM;EAChE,QAAQ;EACR,aAAa;EACb,SAAS,MAAMA,oBAAAA,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,uBAAuB;CACzD,MAAM,OAAQ,MAAM,SAAS,KAAK;CAElC,oBAAA,OAAO,sBAAsB,SAAS,WAAW,OAAO,IAAI,CAAC;CAC7D,oBAAA,MAAM,UAAU;CAChB,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;iBAkEsB,cAAc,QAAQ;iBAetB,WAAW,KAAK,KAAK,4BAAyB,QAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/visitor/index.ts"],"mappings":";;;iBAkEsB,cAAc,QAAQ;iBAetB,WAAW,KAAK,KAAK,4BAAyB,QAAA"}
|
package/dist/visitor/index.mjs
CHANGED
|
@@ -11,12 +11,14 @@ async function createVisitor() {
|
|
|
11
11
|
tags,
|
|
12
12
|
properties: tags
|
|
13
13
|
};
|
|
14
|
-
const
|
|
14
|
+
const response = await fetch(`${config.endpoint}/visitors`, {
|
|
15
15
|
method: "POST",
|
|
16
16
|
credentials: "include",
|
|
17
17
|
headers: await config.getHeaders(),
|
|
18
18
|
body: JSON.stringify(dto)
|
|
19
|
-
})
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) throw new Error(`Failed to create visitor: ${response.status} ${await response.text()}`);
|
|
21
|
+
const data = await response.json();
|
|
20
22
|
if (data.id) config.storage.setItem(keys.visitor_id, data.id);
|
|
21
23
|
return data;
|
|
22
24
|
}
|
|
@@ -41,9 +43,12 @@ async function getVisitor() {
|
|
|
41
43
|
if (cache.visitor) return cache.visitor;
|
|
42
44
|
if (visitorFetcher) return visitorFetcher;
|
|
43
45
|
visitorFetcher = getOrCreateVisitor();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
try {
|
|
47
|
+
cache.visitor = await visitorFetcher;
|
|
48
|
+
return cache.visitor;
|
|
49
|
+
} finally {
|
|
50
|
+
visitorFetcher = null;
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
async function setVisitor(dto) {
|
|
49
54
|
const { id } = await getVisitor();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n cache.visitor = await visitorFetcher;\n visitorFetcher = null;\n
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/visitor/index.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateVisitorDTO, UpdateVisitorDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport type { Visitor, VisitorProperties } from './types';\n\nasync function createVisitor(): Promise<Visitor> {\n const tags = await config.getTags();\n const dto: CreateVisitorDTO = {\n device_id: await config.getDeviceId(),\n platform: config.platform,\n environment: config.environment,\n tags,\n properties: tags as VisitorProperties,\n };\n\n const response = await fetch(`${config.endpoint}/visitors`, {\n method: 'POST',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create visitor: ${response.status} ${await response.text()}`);\n }\n const data = (await response.json()) as Visitor;\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n}\n\nasync function getOrCreateVisitor(): Promise<Visitor> {\n const visitorId = config.storage.getItem(keys.visitor_id);\n if (visitorId && visitorId !== 'undefined') {\n // PATCH, not GET: `tags` is the last-touch counterpart to `initial_tags`,\n // and the only thing that ever refreshed it was `setVisitor`, which hosts\n // call when they identify a user. A visitor who never signs in therefore\n // kept the browser, screen, and release captured on their first ever page\n // load — for the rest of their life — leaving `tags` permanently equal to\n // `initial_tags` and the two columns pointless.\n //\n // Costs nothing extra: this replaces the request that was already here.\n const tags = await config.getTags();\n const response = await fetch(`${config.endpoint}/visitors/${visitorId}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify({ tags } satisfies UpdateVisitorDTO),\n });\n\n if (!response.ok) return createVisitor();\n const data = (await response.json()) as Visitor;\n\n if (data.id) {\n config.storage.setItem(keys.visitor_id, data.id);\n }\n return data;\n } else {\n return createVisitor();\n }\n}\n\nlet visitorFetcher: Promise<Visitor> | null = null;\n\nexport async function getVisitor(): Promise<Visitor> {\n if (cache.visitor) return cache.visitor;\n if (visitorFetcher) return visitorFetcher;\n visitorFetcher = getOrCreateVisitor();\n try {\n cache.visitor = await visitorFetcher;\n return cache.visitor;\n } finally {\n // In a `finally`, so a rejected attempt is not left in `visitorFetcher` for every later\n // caller to await again: `sendEvents` needs a visitor for every batch, and one failed\n // request would otherwise stop the page from reporting anything until it is reloaded.\n visitorFetcher = null;\n }\n}\n\nexport async function setVisitor(dto: Omit<UpdateVisitorDTO, 'tags'>) {\n const { id } = await getVisitor();\n const tags = await config.getTags();\n const body: UpdateVisitorDTO = { ...dto, tags };\n const response = await fetch(`${config.endpoint}/visitors/${id}`, {\n method: 'PATCH',\n credentials: 'include',\n headers: await config.getHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) throw new Error('Failed to set visitor');\n const data = (await response.json()) as Visitor;\n\n config.thirdPartyUserSetters.forEach((setter) => setter(body));\n cache.visitor = data;\n return data;\n}\n"],"mappings":";;;;AAMA,eAAe,gBAAkC;CAC/C,MAAM,OAAO,MAAM,OAAO,QAAQ;CAClC,MAAM,MAAwB;EAC5B,WAAW,MAAM,OAAO,YAAY;EACpC,UAAU,OAAO;EACjB,aAAa,OAAO;EACpB;EACA,YAAY;CACd;CAEA,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY;EAC1D,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,GAAG;CAC1B,CAAC;CAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,6BAA6B,SAAS,OAAO,GAAG,MAAM,SAAS,KAAK,GAAG;CAEzF,MAAM,OAAQ,MAAM,SAAS,KAAK;CAClC,IAAI,KAAK,IACP,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,EAAE;CAEjD,OAAO;AACT;AAEA,eAAe,qBAAuC;CACpD,MAAM,YAAY,OAAO,QAAQ,QAAQ,KAAK,UAAU;CACxD,IAAI,aAAa,cAAc,aAAa;EAS1C,MAAM,OAAO,MAAM,OAAO,QAAQ;EAClC,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY,aAAa;GACvE,QAAQ;GACR,aAAa;GACb,SAAS,MAAM,OAAO,WAAW;GACjC,MAAM,KAAK,UAAU,EAAE,KAAK,CAA4B;EAC1D,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,cAAc;EACvC,MAAM,OAAQ,MAAM,SAAS,KAAK;EAElC,IAAI,KAAK,IACP,OAAO,QAAQ,QAAQ,KAAK,YAAY,KAAK,EAAE;EAEjD,OAAO;CACT,OACE,OAAO,cAAc;AAEzB;AAEA,IAAI,iBAA0C;AAE9C,eAAsB,aAA+B;CACnD,IAAI,MAAM,SAAS,OAAO,MAAM;CAChC,IAAI,gBAAgB,OAAO;CAC3B,iBAAiB,mBAAmB;CACpC,IAAI;EACF,MAAM,UAAU,MAAM;EACtB,OAAO,MAAM;CACf,UAAU;EAIR,iBAAiB;CACnB;AACF;AAEA,eAAsB,WAAW,KAAqC;CACpE,MAAM,EAAE,OAAO,MAAM,WAAW;CAChC,MAAM,OAAO,MAAM,OAAO,QAAQ;CAClC,MAAM,OAAyB;EAAE,GAAG;EAAK;CAAK;CAC9C,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,SAAS,YAAY,MAAM;EAChE,QAAQ;EACR,aAAa;EACb,SAAS,MAAM,OAAO,WAAW;EACjC,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,uBAAuB;CACzD,MAAM,OAAQ,MAAM,SAAS,KAAK;CAElC,OAAO,sBAAsB,SAAS,WAAW,OAAO,IAAI,CAAC;CAC7D,MAAM,UAAU;CAChB,OAAO;AACT"}
|
package/dist/web/index.cjs
CHANGED
|
@@ -9,21 +9,42 @@ let cookie = require("cookie");
|
|
|
9
9
|
//#region src/web/index.ts
|
|
10
10
|
var import_es5 = /* @__PURE__ */ require_runtime.__toESM(require_es5$1.default, 1);
|
|
11
11
|
function getDeviceId() {
|
|
12
|
-
const cached =
|
|
12
|
+
const cached = require_setup_index.config.storage.getItem(require_constants_storage.keys.device_id);
|
|
13
13
|
if (cached) return cached;
|
|
14
14
|
const id = crypto.randomUUID ? crypto.randomUUID() : (0, uuid.v4)();
|
|
15
|
-
|
|
15
|
+
require_setup_index.config.storage.setItem(require_constants_storage.keys.device_id, id);
|
|
16
16
|
return id;
|
|
17
17
|
}
|
|
18
|
+
const links = /* @__PURE__ */ new Map();
|
|
19
|
+
/**
|
|
20
|
+
* `getTags` runs once per event now, and the link a `?s=` id points at cannot change while the
|
|
21
|
+
* page is open, so an uncached lookup would put an identical request on the wire for every
|
|
22
|
+
* event the page sends.
|
|
23
|
+
*/
|
|
24
|
+
function getCachedLink(id) {
|
|
25
|
+
const cached = links.get(id);
|
|
26
|
+
if (cached) return cached;
|
|
27
|
+
const link = require_link_index.getLink(id).then((result) => {
|
|
28
|
+
if (!result) links.delete(id);
|
|
29
|
+
return result;
|
|
30
|
+
});
|
|
31
|
+
links.set(id, link);
|
|
32
|
+
return link;
|
|
33
|
+
}
|
|
34
|
+
/** The user agent cannot change while the page is open, so it is parsed once. */
|
|
35
|
+
let parser;
|
|
18
36
|
async function getTags() {
|
|
19
|
-
const
|
|
37
|
+
const page_location = window.location.href;
|
|
38
|
+
const page_referrer = document.referrer || void 0;
|
|
39
|
+
const page_title = document.title;
|
|
40
|
+
parser ??= import_es5.default.getParser(window.navigator.userAgent);
|
|
20
41
|
const params = new URLSearchParams(window.location.search);
|
|
21
42
|
const os = parser.getOS();
|
|
22
43
|
const browser = parser.getBrowser();
|
|
23
44
|
const platform = parser.getPlatform();
|
|
24
45
|
const parsed = (0, cookie.parseCookie)(document.cookie);
|
|
25
46
|
const linkId = params.get("s");
|
|
26
|
-
const link = linkId ? await
|
|
47
|
+
const link = linkId ? await getCachedLink(linkId) : null;
|
|
27
48
|
const tags = {
|
|
28
49
|
os: `${os.name} ${os.version}`,
|
|
29
50
|
os_name: os.name,
|
|
@@ -42,8 +63,9 @@ async function getTags() {
|
|
|
42
63
|
release: require_setup_index.config.release,
|
|
43
64
|
language: navigator.language,
|
|
44
65
|
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
45
|
-
|
|
46
|
-
page_referrer
|
|
66
|
+
page_location,
|
|
67
|
+
page_referrer,
|
|
68
|
+
page_title,
|
|
47
69
|
fbc: parsed._fbc ?? void 0,
|
|
48
70
|
fbp: parsed._fbp,
|
|
49
71
|
fbclid: params.get("fbclid") ?? void 0,
|