@astroscope/relay 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/README.md +236 -2
- package/dist/awin/index.d.ts +57 -0
- package/dist/awin/index.d.ts.map +1 -0
- package/dist/awin/index.js +54 -0
- package/dist/awin/index.js.map +1 -0
- package/dist/bus-Z7l8oJtS.d.ts +48 -0
- package/dist/bus-Z7l8oJtS.d.ts.map +1 -0
- package/dist/ccm19/index.d.ts +33 -0
- package/dist/ccm19/index.d.ts.map +1 -0
- package/dist/ccm19/index.js +29 -0
- package/dist/ccm19/index.js.map +1 -0
- package/dist/client/index.d.ts +16 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +24 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/track.d.ts +4 -0
- package/dist/client/track.d.ts.map +1 -0
- package/dist/client/track.js +7 -0
- package/dist/client/track.js.map +1 -0
- package/dist/collect-DRl177oR.d.ts +31 -0
- package/dist/collect-DRl177oR.d.ts.map +1 -0
- package/dist/collect-DWdHyn-u.js +45 -0
- package/dist/collect-DWdHyn-u.js.map +1 -0
- package/dist/commerce-DCGO8yag.d.ts +106 -0
- package/dist/commerce-DCGO8yag.d.ts.map +1 -0
- package/dist/commerce-DgNeBWhO.js +46 -0
- package/dist/commerce-DgNeBWhO.js.map +1 -0
- package/dist/context-D11BBDcb.js +27 -0
- package/dist/context-D11BBDcb.js.map +1 -0
- package/dist/conversion-user-CV8xIWDK.js +49 -0
- package/dist/conversion-user-CV8xIWDK.js.map +1 -0
- package/dist/conversion-user-DCfM5EuZ.d.ts +35 -0
- package/dist/conversion-user-DCfM5EuZ.d.ts.map +1 -0
- package/dist/ga4/index.d.ts +50 -0
- package/dist/ga4/index.d.ts.map +1 -0
- package/dist/ga4/index.js +45 -0
- package/dist/ga4/index.js.map +1 -0
- package/dist/gtm/index.d.ts +51 -0
- package/dist/gtm/index.d.ts.map +1 -0
- package/dist/gtm/index.js +49 -0
- package/dist/gtm/index.js.map +1 -0
- package/dist/index.browser.d.ts +8 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +11 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/instance-Bu0EMJ_-.js +268 -0
- package/dist/instance-Bu0EMJ_-.js.map +1 -0
- package/dist/matomo/index.d.ts +46 -0
- package/dist/matomo/index.d.ts.map +1 -0
- package/dist/matomo/index.js +61 -0
- package/dist/matomo/index.js.map +1 -0
- package/dist/openai/index.d.ts +97 -0
- package/dist/openai/index.d.ts.map +1 -0
- package/dist/openai/index.js +79 -0
- package/dist/openai/index.js.map +1 -0
- package/dist/openai/server.d.ts +17 -0
- package/dist/openai/server.d.ts.map +1 -0
- package/dist/openai/server.js +55 -0
- package/dist/openai/server.js.map +1 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +13 -0
- package/dist/react/index.js.map +1 -0
- package/dist/runtime-J8VplGyY.d.ts +48 -0
- package/dist/runtime-J8VplGyY.d.ts.map +1 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +10 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware-entrypoint.d.ts +2 -0
- package/dist/server/middleware-entrypoint.d.ts.map +1 -0
- package/dist/server/middleware-entrypoint.js +25 -0
- package/dist/server/middleware-entrypoint.js.map +1 -0
- package/dist/server/registry.d.ts +9 -0
- package/dist/server/registry.d.ts.map +1 -0
- package/dist/server/registry.js +10 -0
- package/dist/server/registry.js.map +1 -0
- package/dist/server/track.d.ts +10 -0
- package/dist/server/track.d.ts.map +1 -0
- package/dist/server/track.js +90 -0
- package/dist/server/track.js.map +1 -0
- package/dist/shared-CSwqcxPI.d.ts +77 -0
- package/dist/shared-CSwqcxPI.d.ts.map +1 -0
- package/dist/shared-D281USsp.js +106 -0
- package/dist/shared-D281USsp.js.map +1 -0
- package/package.json +133 -3
- package/src/astro/Track.astro +16 -0
- package/src/astro/index.ts +1 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { t as ConversionUser } from "./conversion-user-DCfM5EuZ.js";
|
|
2
|
+
import { n as collect, r as inlineTrack, t as CollectOptions } from "./collect-DRl177oR.js";
|
|
3
|
+
import { _ as WellKnownCreate, a as CommerceItem, b as wellKnownEvents, c as PurchaseParams, d as SearchParams, f as SelectItemParams, g as ViewItemParams, h as ViewItemListParams, i as BeginCheckoutParams, l as RefundParams, m as ViewCartParams, n as AddShippingInfoParams, o as GenerateLeadParams, p as SignUpParams, r as AddToCartParams, s as LoginParams, t as AddPaymentInfoParams, u as RemoveFromCartParams, v as WellKnownName, y as isCommerceEvent } from "./commerce-DCGO8yag.js";
|
|
4
|
+
import { a as DomainEvent, c as RELAY_TRACK_ATTRIBUTE, d as RelayParams, h as trackAttribute, i as DestinationName, l as RelayDestinations, m as defineEvent, n as ConsentRecord, o as EventDefinition, p as TrackInput, r as ConsentSource, s as EventMeta, t as ConsentAnswer, u as RelayEvent } from "./shared-CSwqcxPI.js";
|
|
5
|
+
import { AstroIntegration } from "astro";
|
|
6
|
+
/**
|
|
7
|
+
* Astro integration for the relay. The site's `src/relay/` folder holds the halves: `client.ts`
|
|
8
|
+
* configures the bus and is loaded on every page, `server.ts` registers the server destinations and is
|
|
9
|
+
* what the server-side `track()` reports to. A half without its file is off. The middleware that
|
|
10
|
+
* reads the consent header off every request is always added, so `relayConsent()` works regardless.
|
|
11
|
+
*/
|
|
12
|
+
export default function relayIntegration(): AstroIntegration;
|
|
13
|
+
export { type AddPaymentInfoParams, type AddShippingInfoParams, type AddToCartParams, type BeginCheckoutParams, type CollectOptions, type CommerceItem, type ConsentAnswer, type ConsentRecord, type ConsentSource, type ConversionUser, type DestinationName, type DomainEvent, type EventDefinition, type EventMeta, type GenerateLeadParams, type LoginParams, type PurchaseParams, RELAY_TRACK_ATTRIBUTE, type RefundParams, type RelayDestinations, type RelayEvent, type RelayParams, type RemoveFromCartParams, type SearchParams, type SelectItemParams, type SignUpParams, type TrackInput, type ViewCartParams, type ViewItemListParams, type ViewItemParams, type WellKnownCreate, type WellKnownName, collect, defineEvent, inlineTrack, isCommerceEvent, trackAttribute, wellKnownEvents };
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration.ts"],"mappings":";;;;;;;;;;;wBAgFwB,oBAAoB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { n as inlineTrack, t as collect } from "./collect-DWdHyn-u.js";
|
|
2
|
+
import { n as wellKnownEvents, t as isCommerceEvent } from "./commerce-DgNeBWhO.js";
|
|
3
|
+
import { d as trackAttribute, n as RELAY_TRACK_ATTRIBUTE, r as defineEvent } from "./shared-D281USsp.js";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
// src/integration.ts
|
|
6
|
+
/** the browser half: the bus configuration as its default export, loaded on every page when the file exists */
|
|
7
|
+
const CLIENT_MODULE = "src/relay/client.ts";
|
|
8
|
+
/** the server half: the registry of server destinations, resolved when the file exists */
|
|
9
|
+
const SERVER_MODULE = "src/relay/server.ts";
|
|
10
|
+
const CLIENT_VIRTUAL_MODULE_ID = "virtual:@astroscope/relay/client";
|
|
11
|
+
const RESOLVED_CLIENT_VIRTUAL_MODULE_ID = `\0${CLIENT_VIRTUAL_MODULE_ID}`;
|
|
12
|
+
/** the package's empty registry, which the site's module replaces when it exists */
|
|
13
|
+
const REGISTRY_MODULE_ID = "@astroscope/relay/registry";
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the halves through vite: the injected page script loads the client module when it
|
|
16
|
+
* exists, and the package's empty registry gives way to the site's module when that exists, so the
|
|
17
|
+
* reporter and the site share one instance. The registry carries the destinations' secrets, so the
|
|
18
|
+
* client build must never pull it in.
|
|
19
|
+
*/
|
|
20
|
+
function relayVitePlugin(logger) {
|
|
21
|
+
let reported = false;
|
|
22
|
+
return {
|
|
23
|
+
name: "@astroscope/relay",
|
|
24
|
+
enforce: "pre",
|
|
25
|
+
async resolveId(id, importer) {
|
|
26
|
+
if (id === CLIENT_VIRTUAL_MODULE_ID) return RESOLVED_CLIENT_VIRTUAL_MODULE_ID;
|
|
27
|
+
if (id !== REGISTRY_MODULE_ID) return;
|
|
28
|
+
return (await this.resolve(`/${SERVER_MODULE}`, importer))?.id;
|
|
29
|
+
},
|
|
30
|
+
async load(id) {
|
|
31
|
+
if (id === RESOLVED_CLIENT_VIRTUAL_MODULE_ID) {
|
|
32
|
+
if (!await this.resolve(`/${CLIENT_MODULE}`)) return "";
|
|
33
|
+
return [
|
|
34
|
+
"import { configureRelay } from '@astroscope/relay/client';",
|
|
35
|
+
`import config from ${JSON.stringify(`/${CLIENT_MODULE}`)};`,
|
|
36
|
+
"configureRelay(await config);"
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
if (id.endsWith(`/${SERVER_MODULE}`) && this.environment?.name === "client") throw new Error(`[@astroscope/relay] ${SERVER_MODULE} is server-only — the browser configures the bus in ${CLIENT_MODULE}`);
|
|
40
|
+
},
|
|
41
|
+
async buildStart() {
|
|
42
|
+
if (reported) return;
|
|
43
|
+
reported = true;
|
|
44
|
+
const [client, server] = await Promise.all([this.resolve(`/${CLIENT_MODULE}`), this.resolve(`/${SERVER_MODULE}`)]);
|
|
45
|
+
logger.info(`${client ? `browser bus from ${CLIENT_MODULE}` : "no browser bus"}, ${server ? `server destinations from ${SERVER_MODULE}` : "no server destinations"}`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Astro integration for the relay. The site's `src/relay/` folder holds the halves: `client.ts`
|
|
51
|
+
* configures the bus and is loaded on every page, `server.ts` registers the server destinations and is
|
|
52
|
+
* what the server-side `track()` reports to. A half without its file is off. The middleware that
|
|
53
|
+
* reads the consent header off every request is always added, so `relayConsent()` works regardless.
|
|
54
|
+
*/
|
|
55
|
+
function relayIntegration() {
|
|
56
|
+
return {
|
|
57
|
+
name: "@astroscope/relay",
|
|
58
|
+
hooks: { "astro:config:setup": ({ addMiddleware, config, injectScript, logger, updateConfig }) => {
|
|
59
|
+
addMiddleware({
|
|
60
|
+
order: "pre",
|
|
61
|
+
entrypoint: "@astroscope/relay/middleware"
|
|
62
|
+
});
|
|
63
|
+
updateConfig({ vite: {
|
|
64
|
+
plugins: [relayVitePlugin(logger)],
|
|
65
|
+
ssr: { noExternal: ["@astroscope/relay"] }
|
|
66
|
+
} });
|
|
67
|
+
if (fs.existsSync(new URL(CLIENT_MODULE, config.root))) injectScript("page", `import ${JSON.stringify(CLIENT_VIRTUAL_MODULE_ID)};`);
|
|
68
|
+
} }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export { RELAY_TRACK_ATTRIBUTE, collect, relayIntegration as default, defineEvent, inlineTrack, isCommerceEvent, trackAttribute, wellKnownEvents };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/integration.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { AstroIntegration, AstroIntegrationLogger } from 'astro';\nimport type { Plugin } from 'vite';\n\n/** the browser half: the bus configuration as its default export, loaded on every page when the file exists */\nconst CLIENT_MODULE = 'src/relay/client.ts';\n/** the server half: the registry of server destinations, resolved when the file exists */\nconst SERVER_MODULE = 'src/relay/server.ts';\n\nconst CLIENT_VIRTUAL_MODULE_ID = 'virtual:@astroscope/relay/client';\nconst RESOLVED_CLIENT_VIRTUAL_MODULE_ID = `\\0${CLIENT_VIRTUAL_MODULE_ID}`;\n/** the package's empty registry, which the site's module replaces when it exists */\nconst REGISTRY_MODULE_ID = '@astroscope/relay/registry';\n\n/**\n * Resolves the halves through vite: the injected page script loads the client module when it\n * exists, and the package's empty registry gives way to the site's module when that exists, so the\n * reporter and the site share one instance. The registry carries the destinations' secrets, so the\n * client build must never pull it in.\n */\nfunction relayVitePlugin(logger: AstroIntegrationLogger): Plugin {\n let reported = false;\n\n return {\n name: '@astroscope/relay',\n // before vite's own resolver, which would otherwise settle the registry import on the package's empty one\n enforce: 'pre',\n\n async resolveId(id, importer) {\n if (id === CLIENT_VIRTUAL_MODULE_ID) return RESOLVED_CLIENT_VIRTUAL_MODULE_ID;\n\n if (id !== REGISTRY_MODULE_ID) return;\n\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n const server = await this.resolve(`/${SERVER_MODULE}`, importer);\n\n return server?.id;\n },\n\n async load(id) {\n if (id === RESOLVED_CLIENT_VIRTUAL_MODULE_ID) {\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n const client = await this.resolve(`/${CLIENT_MODULE}`);\n\n if (!client) return '';\n\n return [\n 'import { configureRelay } from \\'@astroscope/relay/client\\';',\n `import config from ${JSON.stringify(`/${CLIENT_MODULE}`)};`,\n 'configureRelay(await config);',\n ].join('\\n');\n }\n\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n if (id.endsWith(`/${SERVER_MODULE}`) && this.environment?.name === 'client') {\n throw new Error(`[@astroscope/relay] ${SERVER_MODULE} is server-only — the browser configures the bus in ${CLIENT_MODULE}`);\n }\n },\n\n async buildStart() {\n if (reported) return;\n\n reported = true;\n\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n const [client, server] = await Promise.all([this.resolve(`/${CLIENT_MODULE}`), this.resolve(`/${SERVER_MODULE}`)]);\n\n logger.info(\n `${client ? `browser bus from ${CLIENT_MODULE}` : 'no browser bus'}, ${server ? `server destinations from ${SERVER_MODULE}` : 'no server destinations'}`,\n );\n },\n };\n}\n\n/**\n * Astro integration for the relay. The site's `src/relay/` folder holds the halves: `client.ts`\n * configures the bus and is loaded on every page, `server.ts` registers the server destinations and is\n * what the server-side `track()` reports to. A half without its file is off. The middleware that\n * reads the consent header off every request is always added, so `relayConsent()` works regardless.\n */\nexport default function relayIntegration(): AstroIntegration {\n return {\n name: '@astroscope/relay',\n hooks: {\n 'astro:config:setup': ({ addMiddleware, config, injectScript, logger, updateConfig }) => {\n addMiddleware({ order: 'pre', entrypoint: '@astroscope/relay/middleware' });\n // bundled into the server build, so the registry import passes through the plugin instead\n // of node's resolution of an externalized package\n updateConfig({ vite: { plugins: [relayVitePlugin(logger)], ssr: { noExternal: ['@astroscope/relay'] } } });\n\n // a site without a browser half ships no script for it\n if (fs.existsSync(new URL(CLIENT_MODULE, config.root))) {\n injectScript('page', `import ${JSON.stringify(CLIENT_VIRTUAL_MODULE_ID)};`);\n }\n },\n },\n };\n}\n"],"mappings":";;;;;;AAKA,MAAM,gBAAgB;;AAEtB,MAAM,gBAAgB;AAEtB,MAAM,2BAA2B;AACjC,MAAM,oCAAoC,KAAK;;AAE/C,MAAM,qBAAqB;;;;;;;AAQ3B,SAAS,gBAAgB,QAAwC;CAC/D,IAAI,WAAW;CAEf,OAAO;EACL,MAAM;EAEN,SAAS;EAET,MAAM,UAAU,IAAI,UAAU;GAC5B,IAAI,OAAO,0BAA0B,OAAO;GAE5C,IAAI,OAAO,oBAAoB;GAK/B,QAAO,MAFc,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,EAAA,EAEhD;EACjB;EAEA,MAAM,KAAK,IAAI;GACb,IAAI,OAAO,mCAAmC;IAI5C,IAAI,CAAC,MAFgB,KAAK,QAAQ,IAAI,eAAe,GAExC,OAAO;IAEpB,OAAO;KACL;KACA,sBAAsB,KAAK,UAAU,IAAI,eAAe,EAAE;KAC1D;IACF,CAAC,CAAC,KAAK,IAAI;GACb;GAGA,IAAI,GAAG,SAAS,IAAI,eAAe,KAAK,KAAK,aAAa,SAAS,UACjE,MAAM,IAAI,MAAM,uBAAuB,cAAc,sDAAsD,eAAe;EAE9H;EAEA,MAAM,aAAa;GACjB,IAAI,UAAU;GAEd,WAAW;GAGX,MAAM,CAAC,QAAQ,UAAU,MAAM,QAAQ,IAAI,CAAC,KAAK,QAAQ,IAAI,eAAe,GAAG,KAAK,QAAQ,IAAI,eAAe,CAAC,CAAC;GAEjH,OAAO,KACL,GAAG,SAAS,oBAAoB,kBAAkB,iBAAiB,IAAI,SAAS,4BAA4B,kBAAkB,0BAChI;EACF;CACF;AACF;;;;;;;AAQA,SAAwB,mBAAqC;CAC3D,OAAO;EACL,MAAM;EACN,OAAO,EACL,uBAAuB,EAAE,eAAe,QAAQ,cAAc,QAAQ,mBAAmB;GACvF,cAAc;IAAE,OAAO;IAAO,YAAY;GAA+B,CAAC;GAG1E,aAAa,EAAE,MAAM;IAAE,SAAS,CAAC,gBAAgB,MAAM,CAAC;IAAG,KAAK,EAAE,YAAY,CAAC,mBAAmB,EAAE;GAAE,EAAE,CAAC;GAGzG,IAAI,GAAG,WAAW,IAAI,IAAI,eAAe,OAAO,IAAI,CAAC,GACnD,aAAa,QAAQ,UAAU,KAAK,UAAU,wBAAwB,EAAE,EAAE;EAE9E,EACF;CACF;AACF"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { a as isBrowser, c as payloadOf, f as unusableEventId, i as eventIdOf, l as serializeConsent, n as RELAY_TRACK_ATTRIBUTE, o as isTrackInput, t as RELAY_CONSENT_HEADER, u as toTracked } from "./shared-D281USsp.js";
|
|
2
|
+
// src/client/debug.ts
|
|
3
|
+
const titleStyle = "color: #5c7ced;";
|
|
4
|
+
const sentStyle = "color: #5c7ced;";
|
|
5
|
+
const pendingStyle = "color: #999;";
|
|
6
|
+
const declinedStyle = "color: #999;text-decoration: line-through;";
|
|
7
|
+
function styleOf(status) {
|
|
8
|
+
switch (status) {
|
|
9
|
+
case "sent": return sentStyle;
|
|
10
|
+
case "declined": return declinedStyle;
|
|
11
|
+
default: return pendingStyle;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function safely(fn) {
|
|
15
|
+
try {
|
|
16
|
+
fn();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("relay: failed printing the debug log", error);
|
|
19
|
+
console.groupEnd();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** one collapsed group per tracked event: what came in, and what each destination got or why not */
|
|
23
|
+
function reportTracked(input, meta, outcomes) {
|
|
24
|
+
safely(() => {
|
|
25
|
+
const sent = outcomes.filter(({ status }) => status === "sent").length;
|
|
26
|
+
console.groupCollapsed(`%crelay ${meta.key ?? "event"} #${meta.id} → sent to ${sent} of ${outcomes.length}`, titleStyle);
|
|
27
|
+
console.log("%c>>", titleStyle, input);
|
|
28
|
+
for (const { name, payload, status } of outcomes) console.log(`%c<< ${name}`, styleOf(status), status === "sent" ? payload : status, status === "sent" ? "" : payload);
|
|
29
|
+
console.groupEnd();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** a delivery after the event's own group: consent given later, an sdk that appeared, a replay on the next page */
|
|
33
|
+
function reportLate(name, payload, meta) {
|
|
34
|
+
safely(() => {
|
|
35
|
+
console.groupCollapsed(`%crelay ${meta.key ?? "event"} #${meta.id} → ${name}, later`, titleStyle);
|
|
36
|
+
console.log(`%c<< ${name}`, sentStyle, payload);
|
|
37
|
+
console.groupEnd();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// src/client/bus.ts
|
|
41
|
+
/** events kept in memory for destinations that are not ready or not consented yet */
|
|
42
|
+
const BUFFER_SIZE = 200;
|
|
43
|
+
const READY_POLL_MS = 200;
|
|
44
|
+
const READY_POLL_ATTEMPTS = 100;
|
|
45
|
+
function createBus(env = {}) {
|
|
46
|
+
const now = env.now ?? (() => Date.now());
|
|
47
|
+
const schedule = env.schedule ?? defaultSchedule;
|
|
48
|
+
let states = [];
|
|
49
|
+
let debug;
|
|
50
|
+
let tracking;
|
|
51
|
+
let consent = {
|
|
52
|
+
names: [],
|
|
53
|
+
of: () => {}
|
|
54
|
+
};
|
|
55
|
+
let free = /* @__PURE__ */ new Set();
|
|
56
|
+
const buffer = [];
|
|
57
|
+
let cancelPoll;
|
|
58
|
+
let pollAttempts = 0;
|
|
59
|
+
const consentOf = (destination) => free.has(destination.name) ? true : consent.of(destination.name);
|
|
60
|
+
/** the consent as known now, by destination name; an unknown answer counts as not given */
|
|
61
|
+
const consentRecord = () => Object.fromEntries(states.filter(({ destination }) => !free.has(destination.name)).map(({ destination }) => [destination.name, consentOf(destination) === true]));
|
|
62
|
+
const dispatch = () => {
|
|
63
|
+
let waiting = false;
|
|
64
|
+
for (const state of states) {
|
|
65
|
+
const { destination } = state;
|
|
66
|
+
const allowed = consentOf(destination);
|
|
67
|
+
if (allowed === void 0) {
|
|
68
|
+
waiting ||= buffer.length > 0;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!allowed) continue;
|
|
72
|
+
const pending = buffer.filter((emitted) => !state.delivered.has(emitted.id)).map((emitted) => pendingOf(emitted, destination)).filter((entry) => entry !== void 0);
|
|
73
|
+
if (destination.ready?.() ?? true) deliver(state, pending);
|
|
74
|
+
else waiting ||= pending.length > 0;
|
|
75
|
+
}
|
|
76
|
+
if (waiting) poll();
|
|
77
|
+
};
|
|
78
|
+
const deliver = (state, entries) => {
|
|
79
|
+
for (const { payload, ...meta } of entries) {
|
|
80
|
+
if (state.delivered.has(meta.id)) continue;
|
|
81
|
+
state.delivered.add(meta.id);
|
|
82
|
+
try {
|
|
83
|
+
state.destination.send(payload, meta);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`relay: destination ${state.destination.name} failed`, error);
|
|
86
|
+
}
|
|
87
|
+
if (tracking !== meta.id && debugging()) reportLate(state.destination.name, payload, meta);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const debugging = () => typeof debug === "function" ? debug() : debug === true;
|
|
91
|
+
/** what became of the event at each destination it carries a payload for, as things stand after the dispatch */
|
|
92
|
+
const outcomesOf = (emitted) => {
|
|
93
|
+
const outcomes = [];
|
|
94
|
+
for (const state of states) {
|
|
95
|
+
const { destination } = state;
|
|
96
|
+
const payload = payloadOf(emitted.event, destination.name);
|
|
97
|
+
if (payload === void 0) continue;
|
|
98
|
+
const status = statusOf(state, emitted.id, consentOf(destination));
|
|
99
|
+
outcomes.push({
|
|
100
|
+
name: destination.name,
|
|
101
|
+
payload,
|
|
102
|
+
status
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return outcomes;
|
|
106
|
+
};
|
|
107
|
+
/** the re-check after a choice or a destination sdk appeared: the poll starts over */
|
|
108
|
+
const notify = () => {
|
|
109
|
+
pollAttempts = 0;
|
|
110
|
+
dispatch();
|
|
111
|
+
};
|
|
112
|
+
const poll = () => {
|
|
113
|
+
if (cancelPoll || pollAttempts >= READY_POLL_ATTEMPTS) return;
|
|
114
|
+
cancelPoll = schedule(() => {
|
|
115
|
+
cancelPoll = void 0;
|
|
116
|
+
pollAttempts += 1;
|
|
117
|
+
dispatch();
|
|
118
|
+
}, READY_POLL_MS);
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
configure(next) {
|
|
122
|
+
const destinations = registeredDestinations(next.destinations);
|
|
123
|
+
const answered = /* @__PURE__ */ new Set([...next.consent.names, ...next.consent.free ?? []]);
|
|
124
|
+
for (const destination of destinations) if (!answered.has(destination.name)) throw new Error(`relay: destination ${destination.name} has no consent source`);
|
|
125
|
+
states = destinations.map((destination) => ({
|
|
126
|
+
destination,
|
|
127
|
+
delivered: /* @__PURE__ */ new Set()
|
|
128
|
+
}));
|
|
129
|
+
consent = next.consent;
|
|
130
|
+
free = new Set(next.consent.free);
|
|
131
|
+
debug = next.debug;
|
|
132
|
+
dispatch();
|
|
133
|
+
next.consent.watch?.(notify);
|
|
134
|
+
},
|
|
135
|
+
track(input) {
|
|
136
|
+
const { event, key, id: own } = toTracked(input);
|
|
137
|
+
const id = eventIdOf(own);
|
|
138
|
+
const unusable = unusableEventId(own, id);
|
|
139
|
+
if (unusable) console.error(`relay: ${unusable}`);
|
|
140
|
+
const emitted = {
|
|
141
|
+
id,
|
|
142
|
+
at: now(),
|
|
143
|
+
key,
|
|
144
|
+
event
|
|
145
|
+
};
|
|
146
|
+
buffer.push(emitted);
|
|
147
|
+
if (buffer.length > BUFFER_SIZE) {
|
|
148
|
+
const evicted = buffer.splice(0, buffer.length - BUFFER_SIZE);
|
|
149
|
+
for (const state of states) for (const entry of evicted) state.delivered.delete(entry.id);
|
|
150
|
+
}
|
|
151
|
+
pollAttempts = 0;
|
|
152
|
+
tracking = emitted.id;
|
|
153
|
+
dispatch();
|
|
154
|
+
tracking = void 0;
|
|
155
|
+
if (debugging()) reportTracked(input, {
|
|
156
|
+
id: emitted.id,
|
|
157
|
+
at: emitted.at,
|
|
158
|
+
key
|
|
159
|
+
}, outcomesOf(emitted));
|
|
160
|
+
return emitted.id;
|
|
161
|
+
},
|
|
162
|
+
notify,
|
|
163
|
+
consent: consentRecord
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function statusOf(state, id, allowed) {
|
|
167
|
+
if (state.delivered.has(id)) return "sent";
|
|
168
|
+
if (allowed === void 0) return "consent pending";
|
|
169
|
+
if (!allowed) return "declined";
|
|
170
|
+
return "waiting for sdk";
|
|
171
|
+
}
|
|
172
|
+
/** the destinations of a record, each checked to sit under its own name */
|
|
173
|
+
function registeredDestinations(destinations) {
|
|
174
|
+
const registered = [];
|
|
175
|
+
for (const [name, destination] of Object.entries(destinations)) {
|
|
176
|
+
if (!destination) continue;
|
|
177
|
+
if (destination.name !== name) throw new Error(`relay: destination ${destination.name} is registered under ${name}`);
|
|
178
|
+
registered.push(destination);
|
|
179
|
+
}
|
|
180
|
+
return registered;
|
|
181
|
+
}
|
|
182
|
+
function pendingOf(emitted, destination) {
|
|
183
|
+
const payload = payloadOf(emitted.event, destination.name);
|
|
184
|
+
return payload === void 0 ? void 0 : {
|
|
185
|
+
id: emitted.id,
|
|
186
|
+
at: emitted.at,
|
|
187
|
+
key: emitted.key,
|
|
188
|
+
payload
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function defaultSchedule(callback, ms) {
|
|
192
|
+
const handle = setTimeout(callback, ms);
|
|
193
|
+
return () => clearTimeout(handle);
|
|
194
|
+
}
|
|
195
|
+
// src/client/fetch.ts
|
|
196
|
+
const PATCHED = Symbol.for("@astroscope/relay.fetch");
|
|
197
|
+
/**
|
|
198
|
+
* Wraps the global `fetch` so every same-origin request carries the consent header, read from the
|
|
199
|
+
* bus at call time: the browser's half of the server-side `track()`. Astro's action client calls
|
|
200
|
+
* `fetch` with no way to add a header per call, which is why the global is wrapped, once; the
|
|
201
|
+
* original is kept and called, so other wrappers chain. Cross-origin requests are left alone — a
|
|
202
|
+
* custom header would be a preflight there and tell a third party what the visitor chose.
|
|
203
|
+
*/
|
|
204
|
+
function installConsentHeader(consent) {
|
|
205
|
+
const target = globalThis;
|
|
206
|
+
const original = target.fetch;
|
|
207
|
+
if (typeof original !== "function" || Object.hasOwn(original, PATCHED)) return;
|
|
208
|
+
const patched = function(input, init) {
|
|
209
|
+
const value = serializeConsent(consent());
|
|
210
|
+
if (!value || !isSameOrigin(input)) return original.call(this, input, init);
|
|
211
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
|
|
212
|
+
headers.set(RELAY_CONSENT_HEADER, value);
|
|
213
|
+
return original.call(this, input, {
|
|
214
|
+
...init,
|
|
215
|
+
headers
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
patched[PATCHED] = true;
|
|
219
|
+
target.fetch = patched;
|
|
220
|
+
}
|
|
221
|
+
function isSameOrigin(input) {
|
|
222
|
+
const { location } = globalThis;
|
|
223
|
+
try {
|
|
224
|
+
return new URL(urlOf(input), location.href).origin === location.origin;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function urlOf(input) {
|
|
230
|
+
if (typeof input === "string") return input;
|
|
231
|
+
return input instanceof URL ? input.href : input.url;
|
|
232
|
+
}
|
|
233
|
+
// src/client/instance.ts
|
|
234
|
+
/** the bus of this page: one instance, whichever module loads first */
|
|
235
|
+
const bus = install();
|
|
236
|
+
function install() {
|
|
237
|
+
const existing = globalThis.__relay__;
|
|
238
|
+
if (existing?.track && existing.configure && existing.notify && existing.consent) return existing;
|
|
239
|
+
const created = createBus();
|
|
240
|
+
const queued = Array.isArray(existing?.q) ? existing.q : [];
|
|
241
|
+
const installed = {
|
|
242
|
+
...created,
|
|
243
|
+
q: { push: (input) => void created.track(input) }
|
|
244
|
+
};
|
|
245
|
+
globalThis.__relay__ = installed;
|
|
246
|
+
for (const input of queued) if (isTrackInput(input)) created.track(input);
|
|
247
|
+
if (isBrowser()) {
|
|
248
|
+
installConsentHeader(created.consent);
|
|
249
|
+
document.addEventListener("click", trackClick, { capture: true });
|
|
250
|
+
}
|
|
251
|
+
return created;
|
|
252
|
+
}
|
|
253
|
+
function trackClick(event) {
|
|
254
|
+
const target = event.target;
|
|
255
|
+
if (!(target instanceof Element)) return;
|
|
256
|
+
const raw = target.closest(`[${CSS.escape(RELAY_TRACK_ATTRIBUTE)}]`)?.getAttribute(RELAY_TRACK_ATTRIBUTE);
|
|
257
|
+
if (!raw) return;
|
|
258
|
+
let parsed;
|
|
259
|
+
try {
|
|
260
|
+
parsed = JSON.parse(raw);
|
|
261
|
+
} catch {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (isTrackInput(parsed)) bus.track(parsed);
|
|
265
|
+
}
|
|
266
|
+
export { createBus as n, bus as t };
|
|
267
|
+
|
|
268
|
+
//# sourceMappingURL=instance-Bu0EMJ_-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-Bu0EMJ_-.js","names":[],"sources":["../src/client/debug.ts","../src/client/bus.ts","../src/client/fetch.ts","../src/client/instance.ts"],"sourcesContent":["import type { EventMeta, TrackInput } from '../shared.js';\n\n/** what became of an event at one destination when it was tracked */\nexport type Outcome = {\n name: string;\n payload: unknown;\n status: 'sent' | 'declined' | 'consent pending' | 'waiting for sdk';\n};\n\nconst titleStyle = 'color: #5c7ced;';\nconst sentStyle = 'color: #5c7ced;';\nconst pendingStyle = 'color: #999;';\nconst declinedStyle = 'color: #999;text-decoration: line-through;';\n\nfunction styleOf(status: Outcome['status']): string {\n switch (status) {\n case 'sent': {\n return sentStyle;\n }\n case 'declined': {\n return declinedStyle;\n }\n default: {\n return pendingStyle;\n }\n }\n}\n\nfunction safely(fn: () => void): void {\n try {\n fn();\n } catch (error) {\n console.error('relay: failed printing the debug log', error);\n console.groupEnd();\n }\n}\n\n/** one collapsed group per tracked event: what came in, and what each destination got or why not */\nexport function reportTracked(input: TrackInput, meta: EventMeta, outcomes: readonly Outcome[]): void {\n safely(() => {\n const sent = outcomes.filter(({ status }) => status === 'sent').length;\n\n console.groupCollapsed(`%crelay ${meta.key ?? 'event'} #${meta.id} → sent to ${sent} of ${outcomes.length}`, titleStyle);\n console.log('%c>>', titleStyle, input);\n\n for (const { name, payload, status } of outcomes) {\n console.log(`%c<< ${name}`, styleOf(status), status === 'sent' ? payload : status, status === 'sent' ? '' : payload);\n }\n\n console.groupEnd();\n });\n}\n\n/** a delivery after the event's own group: consent given later, an sdk that appeared, a replay on the next page */\nexport function reportLate(name: string, payload: unknown, meta: EventMeta): void {\n safely(() => {\n console.groupCollapsed(`%crelay ${meta.key ?? 'event'} #${meta.id} → ${name}, later`, titleStyle);\n console.log(`%c<< ${name}`, sentStyle, payload);\n console.groupEnd();\n });\n}\n","import {\n type ConsentAnswer,\n type ConsentRecord,\n type ConsentSource,\n type DestinationName,\n type EventMeta,\n type RelayDestinations,\n type RelayEvent,\n type TrackInput,\n eventIdOf,\n payloadOf,\n toTracked,\n unusableEventId,\n} from '../shared.js';\nimport { type Outcome, reportLate, reportTracked } from './debug.js';\n\n/** one consumer of the bus: a tag, a pixel, a tracker, a log — it takes the payload built for it */\nexport type Destination<N extends DestinationName = DestinationName> = {\n /** the destination's name: its key in every event and in the registration, its flag in the consent header */\n name: N;\n /** whether the destination's sdk is there to take events; default: always */\n ready?: (() => boolean) | undefined;\n send: (payload: RelayDestinations[N], meta: EventMeta) => void;\n};\n\n/** a destination of any registered name */\nexport type AnyDestination = { [N in DestinationName]: Destination<N> }[DestinationName];\n\n/** the destinations by name: the key an event's payload sits under, which is also the destination's own name */\nexport type Destinations = { [N in DestinationName]?: Destination<N> | undefined };\n\nexport type RelayConfig<P extends Destinations = Destinations> = {\n destinations: P;\n /** the consent of exactly the registered destinations; `NoInfer` keeps `destinations` the only source of the names */\n consent: ConsentSource<NoInfer<keyof P & string>>;\n /**\n * log every tracked event to the console, with what each destination got or why not; a function is asked per event\n */\n debug?: boolean | (() => boolean) | undefined;\n};\n\n/** a consent source for whichever names: what the bus holds, as any `RelayConfig` narrows to it */\ntype ConsentHolder = Omit<ConsentSource, 'of'> & { of: (name: never) => ConsentAnswer };\n\n/** the configuration as the bus takes it, whichever names it was typed for */\nexport type BusConfig = Omit<RelayConfig, 'consent'> & { consent: ConsentHolder };\n\nexport type Bus = {\n /** register the destinations; events tracked earlier are delivered now */\n configure: (config: BusConfig) => void;\n /** put an event on the bus; returns its id */\n track: (input: TrackInput) => string;\n /** re-check the destinations' consent and readiness: after the cmp's choice changed or a destination sdk appeared */\n notify: () => void;\n /** the consent as known now, by destination name: what a request to the server carries */\n consent: () => ConsentRecord;\n};\n\nexport type BusEnvironment = {\n now?: (() => number) | undefined;\n /** schedules a readiness re-check; returns the cancel */\n schedule?: ((callback: () => void, ms: number) => () => void) | undefined;\n};\n\n// the destinations as the bus holds them: the payload type is the destination's business\ntype Registered = {\n name: string;\n ready?: (() => boolean) | undefined;\n send(payload: unknown, meta: EventMeta): void;\n};\n\ntype Emitted = EventMeta & { event: RelayEvent };\n\n/** what a destination gets: its payload of the event, under the event's meta */\ntype Pending = EventMeta & { payload: unknown };\n\ntype DestinationState = {\n destination: Registered;\n delivered: Set<string>;\n};\n\n/** events kept in memory for destinations that are not ready or not consented yet */\nconst BUFFER_SIZE = 200;\n\nconst READY_POLL_MS = 200;\nconst READY_POLL_ATTEMPTS = 100;\n\nexport function createBus(env: BusEnvironment = {}): Bus {\n const now = env.now ?? (() => Date.now());\n const schedule = env.schedule ?? defaultSchedule;\n\n let states: DestinationState[] = [];\n let debug: RelayConfig['debug'];\n // the event being tracked right now: its deliveries belong to its own log group, later ones get their own\n let tracking: string | undefined;\n let consent: ConsentHolder = { names: [], of: () => {} };\n let free = new Set<string>();\n const buffer: Emitted[] = [];\n let cancelPoll: (() => void) | undefined;\n let pollAttempts = 0;\n\n const consentOf = (destination: Registered): ConsentAnswer =>\n free.has(destination.name) ? true : consent.of(destination.name as never);\n\n /** the consent as known now, by destination name; an unknown answer counts as not given */\n const consentRecord = (): ConsentRecord =>\n Object.fromEntries(\n states\n .filter(({ destination }) => !free.has(destination.name))\n .map(({ destination }) => [destination.name, consentOf(destination) === true]),\n );\n\n const dispatch = (): void => {\n let waiting = false;\n\n for (const state of states) {\n const { destination } = state;\n const allowed = consentOf(destination);\n\n // a cmp that has not read the stored choice yet is polled like a destination that has not loaded yet\n if (allowed === undefined) {\n waiting ||= buffer.length > 0;\n continue;\n }\n\n if (!allowed) continue;\n\n const pending = buffer\n .filter((emitted) => !state.delivered.has(emitted.id))\n .map((emitted) => pendingOf(emitted, destination))\n .filter((entry) => entry !== undefined);\n\n if (destination.ready?.() ?? true) {\n deliver(state, pending);\n } else {\n waiting ||= pending.length > 0;\n }\n }\n\n if (waiting) {\n poll();\n }\n };\n\n const deliver = (state: DestinationState, entries: readonly Pending[]): void => {\n for (const { payload, ...meta } of entries) {\n if (state.delivered.has(meta.id)) continue;\n\n state.delivered.add(meta.id);\n\n try {\n state.destination.send(payload, meta);\n } catch (error) {\n console.error(`relay: destination ${state.destination.name} failed`, error);\n }\n\n if (tracking !== meta.id && debugging()) {\n reportLate(state.destination.name, payload, meta);\n }\n }\n };\n\n const debugging = (): boolean => (typeof debug === 'function' ? debug() : debug === true);\n\n /** what became of the event at each destination it carries a payload for, as things stand after the dispatch */\n const outcomesOf = (emitted: Emitted): Outcome[] => {\n const outcomes: Outcome[] = [];\n\n for (const state of states) {\n const { destination } = state;\n const payload = payloadOf(emitted.event, destination.name);\n\n if (payload === undefined) continue;\n\n const status = statusOf(state, emitted.id, consentOf(destination));\n\n outcomes.push({ name: destination.name, payload, status });\n }\n\n return outcomes;\n };\n\n /** the re-check after a choice or a destination sdk appeared: the poll starts over */\n const notify = (): void => {\n pollAttempts = 0;\n dispatch();\n };\n\n const poll = (): void => {\n if (cancelPoll || pollAttempts >= READY_POLL_ATTEMPTS) return;\n\n cancelPoll = schedule(() => {\n cancelPoll = undefined;\n pollAttempts += 1;\n dispatch();\n }, READY_POLL_MS);\n };\n\n return {\n configure(next) {\n const destinations = registeredDestinations(next.destinations);\n const answered = new Set([...next.consent.names, ...(next.consent.free ?? [])]);\n\n for (const destination of destinations) {\n if (!answered.has(destination.name)) {\n throw new Error(`relay: destination ${destination.name} has no consent source`);\n }\n }\n\n states = destinations.map((destination) => ({ destination, delivered: new Set() }));\n consent = next.consent;\n free = new Set(next.consent.free);\n debug = next.debug;\n\n dispatch();\n\n next.consent.watch?.(notify);\n },\n\n track(input) {\n const { event, key, id: own } = toTracked(input);\n const id = eventIdOf(own);\n const unusable = unusableEventId(own, id);\n\n if (unusable) {\n console.error(`relay: ${unusable}`);\n }\n\n const emitted: Emitted = { id, at: now(), key, event };\n\n buffer.push(emitted);\n\n if (buffer.length > BUFFER_SIZE) {\n const evicted = buffer.splice(0, buffer.length - BUFFER_SIZE);\n\n for (const state of states) {\n for (const entry of evicted) state.delivered.delete(entry.id);\n }\n }\n\n pollAttempts = 0;\n tracking = emitted.id;\n dispatch();\n tracking = undefined;\n\n if (debugging()) {\n reportTracked(input, { id: emitted.id, at: emitted.at, key }, outcomesOf(emitted));\n }\n\n return emitted.id;\n },\n\n notify,\n\n consent: consentRecord,\n };\n}\n\nfunction statusOf(state: DestinationState, id: string, allowed: ConsentAnswer): Outcome['status'] {\n if (state.delivered.has(id)) return 'sent';\n if (allowed === undefined) return 'consent pending';\n if (!allowed) return 'declined';\n\n return 'waiting for sdk';\n}\n\n/** the destinations of a record, each checked to sit under its own name */\nfunction registeredDestinations(destinations: Destinations): Registered[] {\n const registered: Registered[] = [];\n\n for (const [name, destination] of Object.entries(destinations) as [string, Registered | undefined][]) {\n if (!destination) continue;\n\n if (destination.name !== name) {\n throw new Error(`relay: destination ${destination.name} is registered under ${name}`);\n }\n\n registered.push(destination);\n }\n\n return registered;\n}\n\nfunction pendingOf(emitted: Emitted, destination: Registered): Pending | undefined {\n const payload = payloadOf(emitted.event, destination.name);\n\n return payload === undefined ? undefined : { id: emitted.id, at: emitted.at, key: emitted.key, payload };\n}\n\nfunction defaultSchedule(callback: () => void, ms: number): () => void {\n const handle = setTimeout(callback, ms);\n\n return () => clearTimeout(handle);\n}\n","import { type ConsentRecord, RELAY_CONSENT_HEADER, serializeConsent } from '../shared.js';\n\nconst PATCHED = Symbol.for('@astroscope/relay.fetch');\n\ntype Fetch = typeof fetch & { [PATCHED]?: true };\n\n/**\n * Wraps the global `fetch` so every same-origin request carries the consent header, read from the\n * bus at call time: the browser's half of the server-side `track()`. Astro's action client calls\n * `fetch` with no way to add a header per call, which is why the global is wrapped, once; the\n * original is kept and called, so other wrappers chain. Cross-origin requests are left alone — a\n * custom header would be a preflight there and tell a third party what the visitor chose.\n */\nexport function installConsentHeader(consent: () => ConsentRecord): void {\n const target = globalThis as { fetch?: Fetch };\n const original = target.fetch;\n\n if (typeof original !== 'function' || Object.hasOwn(original, PATCHED)) return;\n\n const patched: Fetch = function (this: unknown, input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n const value = serializeConsent(consent());\n\n if (!value || !isSameOrigin(input)) {\n return original.call(this, input, init);\n }\n\n const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));\n\n headers.set(RELAY_CONSENT_HEADER, value);\n\n return original.call(this, input, { ...init, headers });\n };\n\n patched[PATCHED] = true;\n target.fetch = patched;\n}\n\nfunction isSameOrigin(input: RequestInfo | URL): boolean {\n const { location } = globalThis;\n\n try {\n return new URL(urlOf(input), location.href).origin === location.origin;\n } catch {\n return false;\n }\n}\n\nfunction urlOf(input: RequestInfo | URL): string {\n if (typeof input === 'string') return input;\n\n return input instanceof URL ? input.href : input.url;\n}\n","import { RELAY_TRACK_ATTRIBUTE, type TrackInput, isBrowser, isTrackInput } from '../shared.js';\nimport { type Bus, createBus } from './bus.js';\nimport { installConsentHeader } from './fetch.js';\n\n/** what inline scripts push into before the bus is installed, and the bus itself afterwards */\ntype Queue = { q: { push: (input: TrackInput) => void } };\n\ntype RelayGlobal = Queue & Partial<Bus>;\n\ndeclare global {\n // the global the inline queue and the bus share\n var __relay__: RelayGlobal | undefined;\n}\n\n/** the bus of this page: one instance, whichever module loads first */\nexport const bus: Bus = install();\n\nfunction install(): Bus {\n const existing = globalThis.__relay__;\n\n if (existing?.track && existing.configure && existing.notify && existing.consent) {\n return existing as Bus;\n }\n\n const created = createBus();\n const queued = Array.isArray(existing?.q) ? (existing.q as unknown[]) : [];\n\n // later inline pushes land on the bus directly\n const installed: RelayGlobal = { ...created, q: { push: (input) => void created.track(input) } };\n\n // eslint-disable-next-line unicorn/no-global-object-property-assignment -- `self.__relay__` is the queue inline scripts push into before the bus exists; the bus takes its place\n globalThis.__relay__ = installed;\n\n for (const input of queued) {\n if (isTrackInput(input)) created.track(input);\n }\n\n if (isBrowser()) {\n installConsentHeader(created.consent);\n document.addEventListener('click', trackClick, { capture: true });\n }\n\n return created;\n}\n\n// an element marked `data-track` (a button, a link) tracks the event serialized into the attribute\n// when clicked — before hydration too, and without a listener per component\nfunction trackClick(event: MouseEvent): void {\n const target = event.target;\n\n if (!(target instanceof Element)) return;\n\n const element = target.closest(`[${CSS.escape(RELAY_TRACK_ATTRIBUTE)}]`);\n const raw = element?.getAttribute(RELAY_TRACK_ATTRIBUTE);\n\n if (!raw) return;\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(raw);\n } catch {\n return;\n }\n\n if (isTrackInput(parsed)) bus.track(parsed);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { a as Destination } from "../bus-Z7l8oJtS.js";
|
|
2
|
+
import { a as CommerceItem } from "../commerce-DCGO8yag.js";
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
/** the matomo command queue; the tracker replays it once loaded */
|
|
6
|
+
_paq?: unknown[][] | undefined;
|
|
7
|
+
/** set by matomo.js once the tracker has loaded */
|
|
8
|
+
Matomo?: unknown;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** one `_paq.push()` argument list: the method name and its arguments */
|
|
12
|
+
export type MatomoCall = readonly unknown[];
|
|
13
|
+
/** what the tracker takes: the calls of one event, pushed in order */
|
|
14
|
+
export type MatomoCalls = readonly MatomoCall[];
|
|
15
|
+
declare module '@astroscope/relay' {
|
|
16
|
+
interface RelayDestinations {
|
|
17
|
+
matomo: MatomoCalls;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export type MatomoOrder = {
|
|
21
|
+
/** the order number */
|
|
22
|
+
id: string;
|
|
23
|
+
value: number;
|
|
24
|
+
tax?: number | undefined;
|
|
25
|
+
shipping?: number | undefined;
|
|
26
|
+
items?: readonly CommerceItem[] | undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The matomo builders: an event, a site search, an ecommerce order, or the raw `calls()`. What an
|
|
30
|
+
* event definition puts under `matomo`. Product and cart views stay events: the ecommerce views
|
|
31
|
+
* need the page view call, which the tracker snippet already made.
|
|
32
|
+
*/
|
|
33
|
+
export declare const matomo: {
|
|
34
|
+
calls: (...calls: MatomoCall[]) => MatomoCalls;
|
|
35
|
+
event: (category: string, action: string, name?: string, value?: number) => MatomoCalls;
|
|
36
|
+
siteSearch: (term: string, category?: string, count?: number) => MatomoCalls;
|
|
37
|
+
order: (order: MatomoOrder) => MatomoCalls;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The matomo tracker as a destination: pushes the `matomo` calls of an event into `_paq`. The queue is
|
|
41
|
+
* installed right away, so pushes never wait for the tracker; the tracker's own snippet (from the
|
|
42
|
+
* cmp after consent, or from the page for a cookieless setup, declared free in the consent source)
|
|
43
|
+
* loads matomo.js and the page view.
|
|
44
|
+
*/
|
|
45
|
+
export declare function matomoTracker(): Destination<'matomo'>;
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/matomo/index.ts"],"mappings":";;QAIQ;YACI;;IAER;;IAEA;;;;YAKQ;;YAGA,uBAAuB;;YAGvB;IACR,QAAQ;;;YAIA;;EAEV;EACA;EACA;EACA;EACA,iBAAiB;;;;;;;qBAQN;EACO,WAAA,OAAA,iBAAe;EACf,QAAA,kBAAM,gBAAgB,eAAe,mBAAmB;EAGvD,aAAA,cAAM,mBAAmB,mBAAmB;EAChD,QAAA,OAAA,gBAAc;;;;;;;;wBAcf,iBAAiB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { a as isBrowser } from "../shared-D281USsp.js";
|
|
2
|
+
// src/matomo/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* The matomo builders: an event, a site search, an ecommerce order, or the raw `calls()`. What an
|
|
5
|
+
* event definition puts under `matomo`. Product and cart views stay events: the ecommerce views
|
|
6
|
+
* need the page view call, which the tracker snippet already made.
|
|
7
|
+
*/
|
|
8
|
+
const matomo = {
|
|
9
|
+
calls: (...calls) => calls,
|
|
10
|
+
event: (category, action, name, value) => [[
|
|
11
|
+
"trackEvent",
|
|
12
|
+
category,
|
|
13
|
+
action,
|
|
14
|
+
name,
|
|
15
|
+
value
|
|
16
|
+
]],
|
|
17
|
+
siteSearch: (term, category, count) => [[
|
|
18
|
+
"trackSiteSearch",
|
|
19
|
+
term,
|
|
20
|
+
category,
|
|
21
|
+
count
|
|
22
|
+
]],
|
|
23
|
+
order: (order) => [...(order.items ?? []).map((item) => [
|
|
24
|
+
"addEcommerceItem",
|
|
25
|
+
item.item_id,
|
|
26
|
+
item.item_name,
|
|
27
|
+
item.item_category,
|
|
28
|
+
item.price,
|
|
29
|
+
item.quantity
|
|
30
|
+
]), [
|
|
31
|
+
"trackEcommerceOrder",
|
|
32
|
+
order.id,
|
|
33
|
+
order.value,
|
|
34
|
+
void 0,
|
|
35
|
+
order.tax,
|
|
36
|
+
order.shipping
|
|
37
|
+
]]
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The matomo tracker as a destination: pushes the `matomo` calls of an event into `_paq`. The queue is
|
|
41
|
+
* installed right away, so pushes never wait for the tracker; the tracker's own snippet (from the
|
|
42
|
+
* cmp after consent, or from the page for a cookieless setup, declared free in the consent source)
|
|
43
|
+
* loads matomo.js and the page view.
|
|
44
|
+
*/
|
|
45
|
+
function matomoTracker() {
|
|
46
|
+
if (isBrowser()) installQueue();
|
|
47
|
+
return {
|
|
48
|
+
name: "matomo",
|
|
49
|
+
ready: () => isBrowser() && globalThis.window.Matomo !== void 0,
|
|
50
|
+
send: (calls) => {
|
|
51
|
+
const paq = globalThis.window._paq ?? [];
|
|
52
|
+
for (const call of calls) paq.push([...call]);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function installQueue() {
|
|
57
|
+
window._paq ??= [];
|
|
58
|
+
}
|
|
59
|
+
export { matomo, matomoTracker };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/matomo/index.ts"],"sourcesContent":["import type { Destination } from '../client/bus.js';\nimport type { CommerceItem } from '../commerce.js';\nimport { isBrowser } from '../shared.js';\n\ndeclare global {\n interface Window {\n /** the matomo command queue; the tracker replays it once loaded */\n _paq?: unknown[][] | undefined;\n /** set by matomo.js once the tracker has loaded */\n Matomo?: unknown;\n }\n}\n\n/** one `_paq.push()` argument list: the method name and its arguments */\nexport type MatomoCall = readonly unknown[];\n\n/** what the tracker takes: the calls of one event, pushed in order */\nexport type MatomoCalls = readonly MatomoCall[];\n\ndeclare module '@astroscope/relay' {\n interface RelayDestinations {\n matomo: MatomoCalls;\n }\n}\n\nexport type MatomoOrder = {\n /** the order number */\n id: string;\n value: number;\n tax?: number | undefined;\n shipping?: number | undefined;\n items?: readonly CommerceItem[] | undefined;\n};\n\n/**\n * The matomo builders: an event, a site search, an ecommerce order, or the raw `calls()`. What an\n * event definition puts under `matomo`. Product and cart views stay events: the ecommerce views\n * need the page view call, which the tracker snippet already made.\n */\nexport const matomo = {\n calls: (...calls: MatomoCall[]): MatomoCalls => calls,\n event: (category: string, action: string, name?: string, value?: number): MatomoCalls => [\n ['trackEvent', category, action, name, value],\n ],\n siteSearch: (term: string, category?: string, count?: number): MatomoCalls => [['trackSiteSearch', term, category, count]],\n order: (order: MatomoOrder): MatomoCalls => [\n ...(order.items ?? []).map(\n (item): MatomoCall => ['addEcommerceItem', item.item_id, item.item_name, item.item_category, item.price, item.quantity],\n ),\n ['trackEcommerceOrder', order.id, order.value, undefined, order.tax, order.shipping],\n ],\n};\n\n/**\n * The matomo tracker as a destination: pushes the `matomo` calls of an event into `_paq`. The queue is\n * installed right away, so pushes never wait for the tracker; the tracker's own snippet (from the\n * cmp after consent, or from the page for a cookieless setup, declared free in the consent source)\n * loads matomo.js and the page view.\n */\nexport function matomoTracker(): Destination<'matomo'> {\n if (isBrowser()) {\n installQueue();\n }\n\n return {\n name: 'matomo',\n ready: () => isBrowser() && globalThis.window.Matomo !== undefined,\n send: (calls) => {\n const paq = globalThis.window._paq ?? [];\n\n for (const call of calls) {\n paq.push([...call]);\n }\n },\n };\n}\n\nfunction installQueue(): void {\n // eslint-disable-next-line unicorn/no-global-object-property-assignment -- `window._paq` is the tracker's documented contract\n window._paq ??= [];\n}\n"],"mappings":";;;;;;;AAuCA,MAAa,SAAS;CACpB,QAAQ,GAAG,UAAqC;CAChD,QAAQ,UAAkB,QAAgB,MAAe,UAAgC,CACvF;EAAC;EAAc;EAAU;EAAQ;EAAM;CAAK,CAC9C;CACA,aAAa,MAAc,UAAmB,UAAgC,CAAC;EAAC;EAAmB;EAAM;EAAU;CAAK,CAAC;CACzH,QAAQ,UAAoC,CAC1C,IAAI,MAAM,SAAS,CAAC,EAAA,CAAG,KACpB,SAAqB;EAAC;EAAoB,KAAK;EAAS,KAAK;EAAW,KAAK;EAAe,KAAK;EAAO,KAAK;CAAQ,CACxH,GACA;EAAC;EAAuB,MAAM;EAAI,MAAM;EAAO,KAAA;EAAW,MAAM;EAAK,MAAM;CAAQ,CACrF;AACF;;;;;;;AAQA,SAAgB,gBAAuC;CACrD,IAAI,UAAU,GACZ,aAAa;CAGf,OAAO;EACL,MAAM;EACN,aAAa,UAAU,KAAK,WAAW,OAAO,WAAW,KAAA;EACzD,OAAO,UAAU;GACf,MAAM,MAAM,WAAW,OAAO,QAAQ,CAAC;GAEvC,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC;EAEtB;CACF;AACF;AAEA,SAAS,eAAqB;CAE5B,OAAO,SAAS,CAAC;AACnB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { a as Destination } from "../bus-Z7l8oJtS.js";
|
|
2
|
+
import { t as ConversionUser } from "../conversion-user-DCfM5EuZ.js";
|
|
3
|
+
/** monetary values are integers in the minor unit of the currency: 12999 for 129.99 EUR */
|
|
4
|
+
type OpenAiContent = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
content_type: string;
|
|
8
|
+
quantity: number;
|
|
9
|
+
amount: number;
|
|
10
|
+
currency: string;
|
|
11
|
+
};
|
|
12
|
+
type OpenAiContentsData = {
|
|
13
|
+
type: 'contents';
|
|
14
|
+
amount: number;
|
|
15
|
+
currency: string;
|
|
16
|
+
contents: readonly OpenAiContent[];
|
|
17
|
+
};
|
|
18
|
+
type OpenAiCustomerActionData = {
|
|
19
|
+
type: 'customer_action';
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* What openai takes, from the pixel and from the conversions api alike: the event and, for a
|
|
23
|
+
* conversion the visitor entered identifiers for, the identifiers — hashed by whichever half sends.
|
|
24
|
+
*/
|
|
25
|
+
type OpenAiEvent = ({
|
|
26
|
+
name: 'contents_viewed' | 'items_added' | 'checkout_started' | 'order_created';
|
|
27
|
+
data: OpenAiContentsData;
|
|
28
|
+
} | {
|
|
29
|
+
name: 'lead_created';
|
|
30
|
+
data: OpenAiCustomerActionData;
|
|
31
|
+
}) & {
|
|
32
|
+
user?: ConversionUser | undefined;
|
|
33
|
+
};
|
|
34
|
+
declare module '@astroscope/relay' {
|
|
35
|
+
interface RelayDestinations {
|
|
36
|
+
openai: OpenAiEvent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
type OpenAiContentInput = {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
price: number;
|
|
43
|
+
quantity: number;
|
|
44
|
+
};
|
|
45
|
+
type OpenAiContents = {
|
|
46
|
+
items: readonly OpenAiContentInput[];
|
|
47
|
+
currency: string;
|
|
48
|
+
/** the total; default: the items summed */
|
|
49
|
+
value?: number | undefined;
|
|
50
|
+
/** the `content_type` of the contents; default `product` */
|
|
51
|
+
contentType?: string | undefined;
|
|
52
|
+
user?: ConversionUser | undefined;
|
|
53
|
+
};
|
|
54
|
+
/** the openai builders: the events openai has a name for. What an event definition puts under `openai` */
|
|
55
|
+
export declare const openai: {
|
|
56
|
+
contentsViewed: (contents: OpenAiContents) => OpenAiEvent;
|
|
57
|
+
itemsAdded: (contents: OpenAiContents) => OpenAiEvent;
|
|
58
|
+
checkoutStarted: (contents: OpenAiContents) => OpenAiEvent;
|
|
59
|
+
orderCreated: (contents: OpenAiContents) => OpenAiEvent;
|
|
60
|
+
leadCreated: (user?: ConversionUser) => OpenAiEvent;
|
|
61
|
+
};
|
|
62
|
+
export declare function toMinorUnits(value: number): number;
|
|
63
|
+
/** advanced matching data of the pixel: normalized identifiers as lowercase sha256 hex */
|
|
64
|
+
type OpenAiUser = {
|
|
65
|
+
email_sha256?: string | undefined;
|
|
66
|
+
phone_number_sha256?: string | undefined;
|
|
67
|
+
first_name_sha256?: string | undefined;
|
|
68
|
+
last_name_sha256?: string | undefined;
|
|
69
|
+
};
|
|
70
|
+
type OpenAiOptions = {
|
|
71
|
+
/** deduplicates against the same event sent through the conversions api */
|
|
72
|
+
event_id?: string | undefined;
|
|
73
|
+
};
|
|
74
|
+
type Oaiq = {
|
|
75
|
+
(command: 'init', options: {
|
|
76
|
+
user: OpenAiUser;
|
|
77
|
+
}): void;
|
|
78
|
+
(command: 'measure', name: OpenAiEvent['name'], data: OpenAiEvent['data'], options?: OpenAiOptions): void;
|
|
79
|
+
};
|
|
80
|
+
declare global {
|
|
81
|
+
interface Window {
|
|
82
|
+
oaiq?: Oaiq | undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export type OpenAiPixelOptions = {
|
|
86
|
+
/** the country calling code national phone numbers get before hashing, e.g. `49` */
|
|
87
|
+
countryCallingCode?: string | undefined;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The openai ads pixel as a destination: measures the `openai` payload of an event under the event's
|
|
91
|
+
* id, so a conversion the server also reported through the conversions api counts once. The pixel
|
|
92
|
+
* is injected by the cmp after consent, so `window.oaiq` shows up late: events wait for it. The
|
|
93
|
+
* identifiers an event carries are hashed and sent through `init` right before it.
|
|
94
|
+
*/
|
|
95
|
+
export declare function openAiPixel(options?: OpenAiPixelOptions): Destination<'openai'>;
|
|
96
|
+
export type { OpenAiContent, OpenAiContentInput, OpenAiContents, OpenAiContentsData, OpenAiCustomerActionData, OpenAiEvent };
|
|
97
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":["_0"],"sources":["../../src/openai/shared.ts","../../src/openai/index.ts"],"mappings":";;;KAKY;EACV;EACA;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA,mBAAmB;;KAGT;EACV;;;;;;KAOU;EAER;EACA,MAAM;;EAGN;EACA,MAAM;;EAGR,OAAO;;;YAIG;IACR,QAAQ;;;KAIA;EAAuB;EAAY;EAAc;EAAe;;KAEhE;EACV,gBAAgB;EAChB;;EAEA;;EAEA;EACA,OAAO;;;qBAII;EACgB,iBAAA,UAAA,mBAAiB;EACrB,aAAA,UAAA,mBAAiB;EACZ,kBAAA,UAAA,mBAAiB;EACpB,eAAA,UAAA,mBAAiB;EACrB,cAAA,OAAA,mBAAiB;;wBA4BxB,aAAa;;KCrFxB;EACH;EACA;EACA;EACA;;KAGG;;EAEH;;KAGG;GACF,iBAAiB;IAAW,MAAM;;GAClC,oBAAoB,MAAM,qBAAqB,MAAM,qBAAqB,UAAU;;QAG/E;YACI;IACR,OAAO;;;YAIC;;EAEV;;;;;;;;wBAWc,YAAY,UAAS,qBAA0B"}
|