@shware/analytics 8.2.0 → 8.3.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/README.md +63 -2
- package/dist/click-id/index.cjs +57 -3
- package/dist/click-id/index.cjs.map +1 -1
- package/dist/click-id/index.d.cts +16 -1
- package/dist/click-id/index.d.cts.map +1 -1
- package/dist/click-id/index.d.mts +16 -1
- package/dist/click-id/index.d.mts.map +1 -1
- package/dist/click-id/index.mjs +54 -4
- package/dist/click-id/index.mjs.map +1 -1
- package/dist/server/google-data-manager.cjs +2 -0
- package/dist/server/google-data-manager.cjs.map +1 -1
- package/dist/server/google-data-manager.d.cts.map +1 -1
- package/dist/server/google-data-manager.d.mts.map +1 -1
- package/dist/server/google-data-manager.mjs +2 -0
- package/dist/server/google-data-manager.mjs.map +1 -1
- package/dist/server/index.cjs +8 -0
- package/dist/server/index.d.cts +5 -4
- package/dist/server/index.d.mts +5 -4
- package/dist/server/index.mjs +5 -4
- package/dist/server/microsoft-conversions-api.cjs +171 -0
- package/dist/server/microsoft-conversions-api.cjs.map +1 -0
- package/dist/server/microsoft-conversions-api.d.cts +117 -0
- package/dist/server/microsoft-conversions-api.d.cts.map +1 -0
- package/dist/server/microsoft-conversions-api.d.mts +117 -0
- package/dist/server/microsoft-conversions-api.d.mts.map +1 -0
- package/dist/server/microsoft-conversions-api.mjs +168 -0
- package/dist/server/microsoft-conversions-api.mjs.map +1 -0
- package/dist/tanstack/middleware.cjs +2 -1
- package/dist/tanstack/middleware.cjs.map +1 -1
- package/dist/tanstack/middleware.d.cts +2 -1
- package/dist/tanstack/middleware.d.cts.map +1 -1
- package/dist/tanstack/middleware.d.mts +2 -1
- package/dist/tanstack/middleware.d.mts.map +1 -1
- package/dist/tanstack/middleware.mjs +2 -1
- package/dist/tanstack/middleware.mjs.map +1 -1
- package/dist/third-parties/index.cjs +5 -0
- package/dist/third-parties/index.d.cts +3 -1
- package/dist/third-parties/index.d.mts +3 -1
- package/dist/third-parties/index.mjs +2 -1
- package/dist/third-parties/microsoft-uet.cjs +119 -0
- package/dist/third-parties/microsoft-uet.cjs.map +1 -0
- package/dist/third-parties/microsoft-uet.d.cts +70 -0
- package/dist/third-parties/microsoft-uet.d.cts.map +1 -0
- package/dist/third-parties/microsoft-uet.d.mts +70 -0
- package/dist/third-parties/microsoft-uet.d.mts.map +1 -0
- package/dist/third-parties/microsoft-uet.mjs +115 -0
- package/dist/third-parties/microsoft-uet.mjs.map +1 -0
- package/dist/third-parties/openai-pixel.cjs +2 -7
- package/dist/third-parties/openai-pixel.cjs.map +1 -1
- package/dist/third-parties/openai-pixel.d.cts.map +1 -1
- package/dist/third-parties/openai-pixel.d.mts.map +1 -1
- package/dist/third-parties/openai-pixel.mjs +1 -6
- package/dist/third-parties/openai-pixel.mjs.map +1 -1
- package/dist/track/uetq.cjs +80 -0
- package/dist/track/uetq.cjs.map +1 -0
- package/dist/track/uetq.d.cts +111 -0
- package/dist/track/uetq.d.cts.map +1 -0
- package/dist/track/uetq.d.mts +111 -0
- package/dist/track/uetq.d.mts.map +1 -0
- package/dist/track/uetq.mjs +78 -0
- package/dist/track/uetq.mjs.map +1 -0
- package/dist/utils/sha256.cjs +19 -0
- package/dist/utils/sha256.cjs.map +1 -0
- package/dist/utils/sha256.d.cts +13 -0
- package/dist/utils/sha256.d.cts.map +1 -0
- package/dist/utils/sha256.d.mts +13 -0
- package/dist/utils/sha256.d.mts.map +1 -0
- package/dist/utils/sha256.mjs +18 -0
- package/dist/utils/sha256.mjs.map +1 -0
- package/dist/web/index.cjs +1 -1
- 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 +2 -2
- package/dist/web/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { TrackEvent, UserProvidedData } from "../track/types.mjs";
|
|
2
|
+
//#region src/server/microsoft-conversions-api.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Microsoft's email normalization: trim, drop the dots and any `+alias` from the user part —
|
|
5
|
+
* for EVERY domain, unlike Google's gmail-only rule — lowercase, SHA-256. A value that is
|
|
6
|
+
* already a SHA-256 hex digest passes through.
|
|
7
|
+
* https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#hashed-identifiers
|
|
8
|
+
*/
|
|
9
|
+
declare function normalizeEmail(input: string): string;
|
|
10
|
+
interface MicrosoftUserData {
|
|
11
|
+
/** Microsoft click id, as a dashed UUID. */
|
|
12
|
+
msclkid?: string;
|
|
13
|
+
/** SHA-256 of the normalized email. */
|
|
14
|
+
em?: string;
|
|
15
|
+
/** SHA-256 of the E.164 phone number. */
|
|
16
|
+
ph?: string;
|
|
17
|
+
/** The SDK visitor id — what the client-side ID Sync pixel (`sendUETIdSync`) sends as `VID`. */
|
|
18
|
+
anonymousId?: string;
|
|
19
|
+
/** SHA-256 of the signed-in user id — what `sendUETIdSync` sends as `UID`. */
|
|
20
|
+
externalId?: string;
|
|
21
|
+
clientUserAgent?: string;
|
|
22
|
+
clientIpAddress?: string;
|
|
23
|
+
idfa?: string;
|
|
24
|
+
gaid?: string;
|
|
25
|
+
}
|
|
26
|
+
interface MicrosoftItem {
|
|
27
|
+
id?: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
price?: number;
|
|
30
|
+
quantity?: number;
|
|
31
|
+
}
|
|
32
|
+
interface MicrosoftCustomData {
|
|
33
|
+
eventCategory?: string;
|
|
34
|
+
eventLabel?: string;
|
|
35
|
+
eventValue?: number;
|
|
36
|
+
searchTerm?: string;
|
|
37
|
+
transactionId?: string;
|
|
38
|
+
value?: number;
|
|
39
|
+
currency?: string;
|
|
40
|
+
items?: MicrosoftItem[];
|
|
41
|
+
itemIds?: string[];
|
|
42
|
+
pageType?: string;
|
|
43
|
+
ecommTotalValue?: number;
|
|
44
|
+
ecommCategory?: string;
|
|
45
|
+
}
|
|
46
|
+
/** One entry of the `data` array POSTed to `/v1/{tagId}/events`. */
|
|
47
|
+
interface MicrosoftEvent {
|
|
48
|
+
eventType: 'custom' | 'pageLoad';
|
|
49
|
+
eventId: string;
|
|
50
|
+
/** The UET action; absent on `pageLoad` events. */
|
|
51
|
+
eventName?: string;
|
|
52
|
+
/** UNIX seconds, UTC; the API rejects anything older than 7 days. */
|
|
53
|
+
eventTime: number;
|
|
54
|
+
eventSourceUrl?: string;
|
|
55
|
+
referrerUrl?: string;
|
|
56
|
+
pageTitle?: string;
|
|
57
|
+
/** `G` granted, `D` denied. Absent means granted. */
|
|
58
|
+
adStorageConsent?: 'G' | 'D';
|
|
59
|
+
userData: MicrosoftUserData;
|
|
60
|
+
customData?: MicrosoftCustomData;
|
|
61
|
+
}
|
|
62
|
+
interface MicrosoftConversionsOptions {
|
|
63
|
+
/**
|
|
64
|
+
* EEA/UK/CH consent for the whole batch. `denied` events are accepted but used for nothing —
|
|
65
|
+
* no attribution, no audiences. Absent means granted, which is also the API's default.
|
|
66
|
+
*/
|
|
67
|
+
consent?: 'granted' | 'denied';
|
|
68
|
+
/**
|
|
69
|
+
* Also send `page_view` events, as CAPI `pageLoad` events. Off by default because the UET
|
|
70
|
+
* JavaScript already reports every page load and the two would double-count destination-URL
|
|
71
|
+
* goals; turn it on for a CAPI-only site with no tag on the page.
|
|
72
|
+
*
|
|
73
|
+
* Not yet modelled: `pageLoadId`, which links custom events to the page load they happened on,
|
|
74
|
+
* and the revenue-only custom event a `pageLoad` needs for a destination goal with variable
|
|
75
|
+
* revenue. Both matter only in this CAPI-only mode.
|
|
76
|
+
*/
|
|
77
|
+
pageLoads?: boolean;
|
|
78
|
+
/** Identifies a third-party sender in Microsoft's monitoring; leave unset for a first-party build. */
|
|
79
|
+
dataProvider?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Process the valid events of a batch and report the invalid ones, instead of the API's default
|
|
82
|
+
* all-or-nothing rejection. On by default — one malformed event should cost itself, not the
|
|
83
|
+
* batch it travelled with.
|
|
84
|
+
*/
|
|
85
|
+
continueOnValidationError?: boolean;
|
|
86
|
+
}
|
|
87
|
+
interface MicrosoftValidationDetail {
|
|
88
|
+
index?: number;
|
|
89
|
+
propertyName?: string;
|
|
90
|
+
attemptedValue?: unknown;
|
|
91
|
+
errorMessage?: string;
|
|
92
|
+
errorCode?: string;
|
|
93
|
+
isWarning?: boolean;
|
|
94
|
+
}
|
|
95
|
+
interface MicrosoftConversionsResponse {
|
|
96
|
+
eventsReceived?: number;
|
|
97
|
+
error?: {
|
|
98
|
+
code?: string;
|
|
99
|
+
message?: string;
|
|
100
|
+
details?: MicrosoftValidationDetail[];
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build one CAPI event. `page_view` becomes a `pageLoad` event (the caller decides whether to
|
|
105
|
+
* send those, see `MicrosoftConversionsOptions.pageLoads`); everything else is a `custom` event
|
|
106
|
+
* named after the track event, which is also the UET action the browser sent.
|
|
107
|
+
*/
|
|
108
|
+
declare function getServerEvent(event: TrackEvent<any>, data?: UserProvidedData, consent?: 'granted' | 'denied'): MicrosoftEvent;
|
|
109
|
+
/**
|
|
110
|
+
* Send events to `POST /v1/{tagId}/events`. Batches above the API's 1,000-event limit are split
|
|
111
|
+
* into consecutive requests; one response is returned per request made. Never throws: an HTTP
|
|
112
|
+
* or network failure is logged (without the token) and yields no response for that request.
|
|
113
|
+
*/
|
|
114
|
+
declare function sendEvents(token: string, tagId: string | number, events: TrackEvent<any>[], data?: UserProvidedData, options?: MicrosoftConversionsOptions): Promise<MicrosoftConversionsResponse[]>;
|
|
115
|
+
//#endregion
|
|
116
|
+
export { MicrosoftConversionsOptions, MicrosoftConversionsResponse, MicrosoftCustomData, MicrosoftEvent, MicrosoftItem, MicrosoftUserData, MicrosoftValidationDetail, getServerEvent, normalizeEmail, sendEvents };
|
|
117
|
+
//# sourceMappingURL=microsoft-conversions-api.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-conversions-api.d.mts","names":[],"sources":["../../src/server/microsoft-conversions-api.ts"],"mappings":";;;;;;;;iBAwCgB,eAAe;UA2Bd;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;;;UAIe;EACf;EACA;;EAEA;;EAEA;EACA;EACA;EACA;;EAEA;EACA,UAAU;EACV,aAAa;;UAGE;;;;;EAKf;;;;;;;;;;EAUA;;EAEA;;;;;;EAMA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;IAAU;IAAe;IAAkB,UAAU;;;;;;;;iBA2DvC,eAEd,OAAO,iBACP,OAAM,kBACN,iCACC;;;;;;iBA4BmB,WACpB,eACA,wBAEA,QAAQ,mBACR,OAAM,kBACN,UAAS,8BACR,QAAQ"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
|
|
2
|
+
import { formatMsclkid } from "../click-id/index.mjs";
|
|
3
|
+
import { pageLocation } from "./page-location.mjs";
|
|
4
|
+
import { mapUETEvent } from "../track/uetq.mjs";
|
|
5
|
+
import { chunk, fetch } from "@shware/utils";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
//#region src/server/microsoft-conversions-api.ts
|
|
8
|
+
/**
|
|
9
|
+
* Microsoft Advertising Conversions API (CAPI) sender — the server-side counterpart of the UET
|
|
10
|
+
* tag, in the same shape as the other senders here: a pure per-event builder, a never-throws
|
|
11
|
+
* `sendEvents`, the token in a header and never in a URL or a log line.
|
|
12
|
+
*
|
|
13
|
+
* Microsoft's recommended deployment is hybrid: keep the UET JavaScript on the page and send the
|
|
14
|
+
* same conversions here too, deduplicated by (`tagId`, `eventName`, `eventId`). Both channels in
|
|
15
|
+
* this SDK are fed from the same `track()` call — the browser sends the internal name as the UET
|
|
16
|
+
* action with the event id as `event_id`, and this builder sends the same name as `eventName`
|
|
17
|
+
* with the same id — so the pair agrees by construction.
|
|
18
|
+
*
|
|
19
|
+
* The token comes from the UET tag's "Use Conversions API" setup step, or from the Campaign
|
|
20
|
+
* Management API's `UetTagAuthKey/Query`. Access is provisioned per customer account: an account
|
|
21
|
+
* whose tag setup has no "Use Conversions API" option has to ask Microsoft Advertising support.
|
|
22
|
+
*
|
|
23
|
+
* https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration
|
|
24
|
+
*/
|
|
25
|
+
const ENDPOINT = "https://capi.uet.microsoft.com/v1";
|
|
26
|
+
/** The API's hard limit per request. */
|
|
27
|
+
const MAX_EVENTS_PER_REQUEST = 1e3;
|
|
28
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
29
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
30
|
+
/**
|
|
31
|
+
* Microsoft's email normalization: trim, drop the dots and any `+alias` from the user part —
|
|
32
|
+
* for EVERY domain, unlike Google's gmail-only rule — lowercase, SHA-256. A value that is
|
|
33
|
+
* already a SHA-256 hex digest passes through.
|
|
34
|
+
* https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#hashed-identifiers
|
|
35
|
+
*/
|
|
36
|
+
function normalizeEmail(input) {
|
|
37
|
+
const email = input.trim().toLowerCase();
|
|
38
|
+
const at = email.lastIndexOf("@");
|
|
39
|
+
if (at === -1) return email;
|
|
40
|
+
return `${email.slice(0, at).replace(/\./g, "").split("+", 1)[0] ?? ""}@${email.slice(at + 1)}`;
|
|
41
|
+
}
|
|
42
|
+
function hashEmail(input) {
|
|
43
|
+
const value = input?.trim().toLowerCase();
|
|
44
|
+
if (!value) return void 0;
|
|
45
|
+
return SHA256.test(value) ? value : sha256(normalizeEmail(value));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Phones are E.164 by the schema (`+` then 11–15 digits, the range bat.js validates too); the
|
|
49
|
+
* separators bat.js tolerates are stripped, and anything else is dropped rather than guessed —
|
|
50
|
+
* a number with no country code hashed under an assumed one can never match.
|
|
51
|
+
*/
|
|
52
|
+
function hashPhone(input) {
|
|
53
|
+
const value = input?.trim().toLowerCase();
|
|
54
|
+
if (!value) return void 0;
|
|
55
|
+
if (SHA256.test(value)) return value;
|
|
56
|
+
const phone = value.replace(/[-() \t]/g, "");
|
|
57
|
+
return /^\+\d{11,15}$/.test(phone) ? sha256(phone) : void 0;
|
|
58
|
+
}
|
|
59
|
+
function getUserData(event, data) {
|
|
60
|
+
const { tags, platform, visitor_id } = event;
|
|
61
|
+
const email = Array.isArray(data.email) ? data.email[0] : data.email;
|
|
62
|
+
const phone = Array.isArray(data.phone_number) ? data.phone_number[0] : data.phone_number;
|
|
63
|
+
return {
|
|
64
|
+
msclkid: tags.msclkid ? formatMsclkid(tags.msclkid) : void 0,
|
|
65
|
+
em: hashEmail(email),
|
|
66
|
+
ph: hashPhone(phone),
|
|
67
|
+
anonymousId: visitor_id || void 0,
|
|
68
|
+
externalId: data.user_id ? sha256(data.user_id) : void 0,
|
|
69
|
+
clientUserAgent: data.user_agent,
|
|
70
|
+
clientIpAddress: typeof tags.ip_address === "string" && tags.ip_address ? tags.ip_address : data.ip_address,
|
|
71
|
+
idfa: platform === "ios" ? tags.advertising_id : void 0,
|
|
72
|
+
gaid: platform === "android" ? tags.advertising_id : void 0
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function getCustomData(event) {
|
|
76
|
+
const [, params] = mapUETEvent(event.name, event.properties);
|
|
77
|
+
const customData = {
|
|
78
|
+
eventCategory: params.event_category,
|
|
79
|
+
eventLabel: params.event_label,
|
|
80
|
+
eventValue: params.event_value,
|
|
81
|
+
searchTerm: params.search_term,
|
|
82
|
+
transactionId: params.transaction_id,
|
|
83
|
+
value: params.revenue_value,
|
|
84
|
+
currency: params.currency,
|
|
85
|
+
items: params.items?.map(({ id, name, price, quantity }) => ({
|
|
86
|
+
id,
|
|
87
|
+
name,
|
|
88
|
+
price,
|
|
89
|
+
quantity
|
|
90
|
+
})),
|
|
91
|
+
itemIds: Array.isArray(params.ecomm_prodid) ? params.ecomm_prodid : params.ecomm_prodid ? [params.ecomm_prodid] : void 0,
|
|
92
|
+
pageType: params.ecomm_pagetype,
|
|
93
|
+
ecommTotalValue: params.ecomm_totalvalue,
|
|
94
|
+
ecommCategory: params.ecomm_category
|
|
95
|
+
};
|
|
96
|
+
return Object.values(customData).some((value) => value !== void 0) ? customData : void 0;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build one CAPI event. `page_view` becomes a `pageLoad` event (the caller decides whether to
|
|
100
|
+
* send those, see `MicrosoftConversionsOptions.pageLoads`); everything else is a `custom` event
|
|
101
|
+
* named after the track event, which is also the UET action the browser sent.
|
|
102
|
+
*/
|
|
103
|
+
function getServerEvent(event, data = {}, consent) {
|
|
104
|
+
const { id, name, tags, created_at } = event;
|
|
105
|
+
const isPageLoad = name === "page_view";
|
|
106
|
+
return {
|
|
107
|
+
eventType: isPageLoad ? "pageLoad" : "custom",
|
|
108
|
+
eventId: tags.idempotency_key ?? id,
|
|
109
|
+
eventName: isPageLoad ? void 0 : name,
|
|
110
|
+
eventTime: Math.round(new Date(created_at).getTime() / 1e3),
|
|
111
|
+
eventSourceUrl: pageLocation(tags),
|
|
112
|
+
referrerUrl: tags.page_referrer,
|
|
113
|
+
pageTitle: tags.page_title,
|
|
114
|
+
adStorageConsent: consent === "granted" ? "G" : consent === "denied" ? "D" : void 0,
|
|
115
|
+
userData: getUserData(event, data),
|
|
116
|
+
customData: isPageLoad ? void 0 : getCustomData(event)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Strip undefined fields so the wire payload carries only what was set. */
|
|
120
|
+
function compact(value) {
|
|
121
|
+
return JSON.parse(JSON.stringify(value));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Send events to `POST /v1/{tagId}/events`. Batches above the API's 1,000-event limit are split
|
|
125
|
+
* into consecutive requests; one response is returned per request made. Never throws: an HTTP
|
|
126
|
+
* or network failure is logged (without the token) and yields no response for that request.
|
|
127
|
+
*/
|
|
128
|
+
async function sendEvents(token, tagId, events, data = {}, options = {}) {
|
|
129
|
+
const { consent, pageLoads = false, dataProvider, continueOnValidationError = true } = options;
|
|
130
|
+
const capiEvents = events.filter((event) => pageLoads && event.name === "page_view" || !IGNORED_EVENTS.includes(event.name)).map((event) => getServerEvent(event, data, consent)).filter((event) => event.eventType !== "pageLoad" || event.eventSourceUrl);
|
|
131
|
+
if (capiEvents.length === 0) return [];
|
|
132
|
+
const responses = [];
|
|
133
|
+
for (const batch of chunk(capiEvents, MAX_EVENTS_PER_REQUEST)) {
|
|
134
|
+
const body = compact({
|
|
135
|
+
data: batch,
|
|
136
|
+
continueOnValidationError,
|
|
137
|
+
dataProvider
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(`${ENDPOINT}/${tagId}/events`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
Accept: "application/json",
|
|
145
|
+
Authorization: `Bearer ${token}`
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify(body)
|
|
148
|
+
});
|
|
149
|
+
if (response.ok) {
|
|
150
|
+
const result = await response.json();
|
|
151
|
+
if (result.error?.details?.length) console.warn(`Microsoft conversion accepted with validation issues: ${JSON.stringify(result.error.details)}`);
|
|
152
|
+
responses.push(result);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const { status } = response;
|
|
156
|
+
const message = await response.text();
|
|
157
|
+
console.error(`Failed to send Microsoft conversion, status: ${status}, body: ${message}`);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
160
|
+
console.error(`Failed to send Microsoft conversion, network error: ${message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return responses;
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { getServerEvent, normalizeEmail, sendEvents };
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=microsoft-conversions-api.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-conversions-api.mjs","names":[],"sources":["../../src/server/microsoft-conversions-api.ts"],"sourcesContent":["/**\n * Microsoft Advertising Conversions API (CAPI) sender — the server-side counterpart of the UET\n * tag, in the same shape as the other senders here: a pure per-event builder, a never-throws\n * `sendEvents`, the token in a header and never in a URL or a log line.\n *\n * Microsoft's recommended deployment is hybrid: keep the UET JavaScript on the page and send the\n * same conversions here too, deduplicated by (`tagId`, `eventName`, `eventId`). Both channels in\n * this SDK are fed from the same `track()` call — the browser sends the internal name as the UET\n * action with the event id as `event_id`, and this builder sends the same name as `eventName`\n * with the same id — so the pair agrees by construction.\n *\n * The token comes from the UET tag's \"Use Conversions API\" setup step, or from the Campaign\n * Management API's `UetTagAuthKey/Query`. Access is provisioned per customer account: an account\n * whose tag setup has no \"Use Conversions API\" option has to ask Microsoft Advertising support.\n *\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration\n */\nimport { createHash } from 'node:crypto';\nimport { chunk, fetch } from '@shware/utils';\nimport { formatMsclkid } from '../click-id/index';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { mapUETEvent } from '../track/uetq';\nimport { pageLocation } from './page-location';\n\nconst ENDPOINT = 'https://capi.uet.microsoft.com/v1';\n\n/** The API's hard limit per request. */\nconst MAX_EVENTS_PER_REQUEST = 1000;\n\nconst SHA256 = /^[a-f0-9]{64}$/;\n\nconst sha256 = (value: string) => createHash('sha256').update(value).digest('hex');\n\n/**\n * Microsoft's email normalization: trim, drop the dots and any `+alias` from the user part —\n * for EVERY domain, unlike Google's gmail-only rule — lowercase, SHA-256. A value that is\n * already a SHA-256 hex digest passes through.\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#hashed-identifiers\n */\nexport function normalizeEmail(input: string): string {\n const email = input.trim().toLowerCase();\n const at = email.lastIndexOf('@');\n if (at === -1) return email;\n const user = email.slice(0, at).replace(/\\./g, '').split('+', 1)[0] ?? '';\n return `${user}@${email.slice(at + 1)}`;\n}\n\nfunction hashEmail(input: string | undefined): string | undefined {\n const value = input?.trim().toLowerCase();\n if (!value) return undefined;\n return SHA256.test(value) ? value : sha256(normalizeEmail(value));\n}\n\n/**\n * Phones are E.164 by the schema (`+` then 11–15 digits, the range bat.js validates too); the\n * separators bat.js tolerates are stripped, and anything else is dropped rather than guessed —\n * a number with no country code hashed under an assumed one can never match.\n */\nfunction hashPhone(input: string | undefined): string | undefined {\n const value = input?.trim().toLowerCase();\n if (!value) return undefined;\n if (SHA256.test(value)) return value;\n const phone = value.replace(/[-() \\t]/g, '');\n return /^\\+\\d{11,15}$/.test(phone) ? sha256(phone) : undefined;\n}\n\nexport interface MicrosoftUserData {\n /** Microsoft click id, as a dashed UUID. */\n msclkid?: string;\n /** SHA-256 of the normalized email. */\n em?: string;\n /** SHA-256 of the E.164 phone number. */\n ph?: string;\n /** The SDK visitor id — what the client-side ID Sync pixel (`sendUETIdSync`) sends as `VID`. */\n anonymousId?: string;\n /** SHA-256 of the signed-in user id — what `sendUETIdSync` sends as `UID`. */\n externalId?: string;\n clientUserAgent?: string;\n clientIpAddress?: string;\n idfa?: string;\n gaid?: string;\n}\n\nexport interface MicrosoftItem {\n id?: string;\n name?: string;\n price?: number;\n quantity?: number;\n}\n\nexport interface MicrosoftCustomData {\n eventCategory?: string;\n eventLabel?: string;\n eventValue?: number;\n searchTerm?: string;\n transactionId?: string;\n value?: number;\n currency?: string;\n items?: MicrosoftItem[];\n itemIds?: string[];\n pageType?: string;\n ecommTotalValue?: number;\n ecommCategory?: string;\n}\n\n/** One entry of the `data` array POSTed to `/v1/{tagId}/events`. */\nexport interface MicrosoftEvent {\n eventType: 'custom' | 'pageLoad';\n eventId: string;\n /** The UET action; absent on `pageLoad` events. */\n eventName?: string;\n /** UNIX seconds, UTC; the API rejects anything older than 7 days. */\n eventTime: number;\n eventSourceUrl?: string;\n referrerUrl?: string;\n pageTitle?: string;\n /** `G` granted, `D` denied. Absent means granted. */\n adStorageConsent?: 'G' | 'D';\n userData: MicrosoftUserData;\n customData?: MicrosoftCustomData;\n}\n\nexport interface MicrosoftConversionsOptions {\n /**\n * EEA/UK/CH consent for the whole batch. `denied` events are accepted but used for nothing —\n * no attribution, no audiences. Absent means granted, which is also the API's default.\n */\n consent?: 'granted' | 'denied';\n /**\n * Also send `page_view` events, as CAPI `pageLoad` events. Off by default because the UET\n * JavaScript already reports every page load and the two would double-count destination-URL\n * goals; turn it on for a CAPI-only site with no tag on the page.\n *\n * Not yet modelled: `pageLoadId`, which links custom events to the page load they happened on,\n * and the revenue-only custom event a `pageLoad` needs for a destination goal with variable\n * revenue. Both matter only in this CAPI-only mode.\n */\n pageLoads?: boolean;\n /** Identifies a third-party sender in Microsoft's monitoring; leave unset for a first-party build. */\n dataProvider?: string;\n /**\n * Process the valid events of a batch and report the invalid ones, instead of the API's default\n * all-or-nothing rejection. On by default — one malformed event should cost itself, not the\n * batch it travelled with.\n */\n continueOnValidationError?: boolean;\n}\n\nexport interface MicrosoftValidationDetail {\n index?: number;\n propertyName?: string;\n attemptedValue?: unknown;\n errorMessage?: string;\n errorCode?: string;\n isWarning?: boolean;\n}\n\nexport interface MicrosoftConversionsResponse {\n eventsReceived?: number;\n error?: { code?: string; message?: string; details?: MicrosoftValidationDetail[] };\n}\n\nfunction getUserData(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData\n): MicrosoftUserData {\n const { tags, platform, visitor_id } = event;\n const email = Array.isArray(data.email) ? data.email[0] : data.email;\n const phone = Array.isArray(data.phone_number) ? data.phone_number[0] : data.phone_number;\n\n return {\n msclkid: tags.msclkid ? formatMsclkid(tags.msclkid) : undefined,\n em: hashEmail(email),\n ph: hashPhone(phone),\n // Microsoft requires this to equal the `VID` of the client-side ID Sync pixel; both sides use\n // the SDK visitor id, which every event carries and the client can always read.\n anonymousId: visitor_id || undefined,\n // Microsoft asks for an anonymized id and never a real one. SHA-256, the same digest\n // `sendUETIdSync` applies client-side, so `externalId` and the pixel's `UID` agree too.\n externalId: data.user_id ? sha256(data.user_id) : undefined,\n clientUserAgent: data.user_agent,\n clientIpAddress:\n typeof tags.ip_address === 'string' && tags.ip_address ? tags.ip_address : data.ip_address,\n idfa: platform === 'ios' ? tags.advertising_id : undefined,\n gaid: platform === 'android' ? tags.advertising_id : undefined,\n };\n}\n\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getCustomData(event: TrackEvent<any>): MicrosoftCustomData | undefined {\n const [, params] = mapUETEvent(event.name, event.properties);\n const customData: MicrosoftCustomData = {\n eventCategory: params.event_category,\n eventLabel: params.event_label,\n eventValue: params.event_value,\n searchTerm: params.search_term,\n transactionId: params.transaction_id,\n value: params.revenue_value,\n currency: params.currency,\n items: params.items?.map(({ id, name, price, quantity }) => ({ id, name, price, quantity })),\n itemIds: Array.isArray(params.ecomm_prodid)\n ? params.ecomm_prodid\n : params.ecomm_prodid\n ? [params.ecomm_prodid]\n : undefined,\n pageType: params.ecomm_pagetype,\n ecommTotalValue: params.ecomm_totalvalue,\n ecommCategory: params.ecomm_category,\n };\n return Object.values(customData).some((value) => value !== undefined) ? customData : undefined;\n}\n\n/**\n * Build one CAPI event. `page_view` becomes a `pageLoad` event (the caller decides whether to\n * send those, see `MicrosoftConversionsOptions.pageLoads`); everything else is a `custom` event\n * named after the track event, which is also the UET action the browser sent.\n */\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData = {},\n consent?: 'granted' | 'denied'\n): MicrosoftEvent {\n const { id, name, tags, created_at } = event;\n const isPageLoad = name === 'page_view';\n\n return {\n eventType: isPageLoad ? 'pageLoad' : 'custom',\n eventId: tags.idempotency_key ?? id,\n eventName: isPageLoad ? undefined : name,\n eventTime: Math.round(new Date(created_at).getTime() / 1000),\n eventSourceUrl: pageLocation(tags),\n referrerUrl: tags.page_referrer,\n pageTitle: tags.page_title,\n adStorageConsent: consent === 'granted' ? 'G' : consent === 'denied' ? 'D' : undefined,\n userData: getUserData(event, data),\n customData: isPageLoad ? undefined : getCustomData(event),\n };\n}\n\n/** Strip undefined fields so the wire payload carries only what was set. */\nfunction compact<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n\n/**\n * Send events to `POST /v1/{tagId}/events`. Batches above the API's 1,000-event limit are split\n * into consecutive requests; one response is returned per request made. Never throws: an HTTP\n * or network failure is logged (without the token) and yields no response for that request.\n */\nexport async function sendEvents(\n token: string,\n tagId: string | number,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n options: MicrosoftConversionsOptions = {}\n): Promise<MicrosoftConversionsResponse[]> {\n const { consent, pageLoads = false, dataProvider, continueOnValidationError = true } = options;\n\n const capiEvents = events\n .filter(\n (event) => (pageLoads && event.name === 'page_view') || !IGNORED_EVENTS.includes(event.name)\n )\n .map((event) => getServerEvent(event, data, consent))\n // A pageLoad without a URL is rejected by the API, and a page-less event has nothing to say.\n .filter((event) => event.eventType !== 'pageLoad' || event.eventSourceUrl);\n if (capiEvents.length === 0) return [];\n\n const responses: MicrosoftConversionsResponse[] = [];\n for (const batch of chunk(capiEvents, MAX_EVENTS_PER_REQUEST)) {\n const body = compact({ data: batch, continueOnValidationError, dataProvider });\n try {\n const response = await fetch(`${ENDPOINT}/${tagId}/events`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(body),\n });\n if (response.ok) {\n const result = (await response.json()) as MicrosoftConversionsResponse;\n // A 200 can still carry field-level warnings (the field was removed) and, with\n // `continueOnValidationError`, skipped events. Surface them: silent partial data loss is\n // the one failure mode the caller cannot see from the status code.\n if (result.error?.details?.length) {\n console.warn(\n `Microsoft conversion accepted with validation issues: ${JSON.stringify(result.error.details)}`\n );\n }\n responses.push(result);\n continue;\n }\n const { status } = response;\n const message = await response.text();\n console.error(`Failed to send Microsoft conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`Failed to send Microsoft conversion, network error: ${message}`);\n }\n }\n return responses;\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,8 @@ const require_click_id_index = require("../click-id/index.cjs");
|
|
|
3
3
|
let _tanstack_react_start = require("@tanstack/react-start");
|
|
4
4
|
//#region src/tanstack/middleware.ts
|
|
5
5
|
/**
|
|
6
|
-
* TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `
|
|
6
|
+
* TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`,
|
|
7
|
+
* `_rdt_cid`, `_uetmsclkid`) on the
|
|
7
8
|
* document response.
|
|
8
9
|
*
|
|
9
10
|
* Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.cjs","names":["createMiddleware","resolveClickIdCookies","toSetCookieHeaders"],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /** `Domain` attribute for the cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (com=0, example.com=1, www.example.com=2). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` on every request as an ITP self-heal (restores the long-lived\n * HTTP cookie if the Meta Pixel's `document.cookie` write re-capped it to 24h in Safari). On by\n * default. Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page\n * response carrying an `_fbc`, defeating CDN caching of those pages; set false to strictly follow\n * Meta's conditional-write rule and keep them cacheable. See {@link resolveClickIdCookies}.\n */\n refresh?: boolean;\n /**\n * Override the `Cache-Control` of a response we attach cookies to (default `private, no-store`).\n * A per-user `Set-Cookie` must never end up on a shared-cache entry, or one visitor's `_fbc` would\n * be served to everyone. Only set this false if you guarantee these responses are never cached.\n */\n cacheControl?: string | false;\n /**\n * Consent gate. Return false to skip setting cookies for this request (e.g. before the visitor has\n * granted consent where required). Runs per request with the incoming `Request`.\n */\n shouldPersist?: (request: Request) => boolean;\n}\n\n/**\n * TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_rdt_cid`) on the\n * document response.\n *\n * Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS\n * runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for\n * 90 days in Safari: ITP caps JavaScript-set cookies on a fbclid-decorated landing page to 24\n * hours, and a document response is never classified as CNAME/IP cloaking (it is the reference the\n * browser measures cloaking against).\n *\n * Register it as a global request middleware:\n * ```ts\n * // start.ts\n * import { createStart } from '@tanstack/react-start'\n * import { clickIdMiddleware } from '@shware/analytics/tanstack'\n * export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))\n * ```\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\nexport function createClickIdMiddleware(options: ClickIdMiddlewareOptions = {}) {\n const { cacheControl = 'private, no-store' } = options;\n\n return createMiddleware({ type: 'request' }).server(async ({ request, next, handlerType }) => {\n const result = await next();\n\n // Skip serverFn RPC responses. 'router' covers SSR document requests *and* custom server\n // routes (API endpoints) — those also get cookies when the URL carries a click id.\n if (handlerType !== 'router') return result;\n if (options.shouldPersist && !options.shouldPersist(request)) return result;\n\n const { cookies } = resolveClickIdCookies({\n url: request.url,\n cookieHeader: request.headers.get('cookie'),\n domain: options.domain,\n secure: options.secure,\n subdomainIndex: options.subdomainIndex,\n refresh: options.refresh ?? true,\n });\n\n if (cookies.length > 0) {\n for (const header of toSetCookieHeaders(cookies)) {\n result.response.headers.append('set-cookie', header);\n }\n if (cacheControl !== false) {\n result.response.headers.set('cache-control', cacheControl);\n }\n }\n\n return result;\n });\n}\n\n/** Ready-to-register middleware with default options. */\nexport const clickIdMiddleware = createClickIdMiddleware();\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"middleware.cjs","names":["createMiddleware","resolveClickIdCookies","toSetCookieHeaders"],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /** `Domain` attribute for the cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (com=0, example.com=1, www.example.com=2). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` on every request as an ITP self-heal (restores the long-lived\n * HTTP cookie if the Meta Pixel's `document.cookie` write re-capped it to 24h in Safari). On by\n * default. Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page\n * response carrying an `_fbc`, defeating CDN caching of those pages; set false to strictly follow\n * Meta's conditional-write rule and keep them cacheable. See {@link resolveClickIdCookies}.\n */\n refresh?: boolean;\n /**\n * Override the `Cache-Control` of a response we attach cookies to (default `private, no-store`).\n * A per-user `Set-Cookie` must never end up on a shared-cache entry, or one visitor's `_fbc` would\n * be served to everyone. Only set this false if you guarantee these responses are never cached.\n */\n cacheControl?: string | false;\n /**\n * Consent gate. Return false to skip setting cookies for this request (e.g. before the visitor has\n * granted consent where required). Runs per request with the incoming `Request`.\n */\n shouldPersist?: (request: Request) => boolean;\n}\n\n/**\n * TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`,\n * `_rdt_cid`, `_uetmsclkid`) on the\n * document response.\n *\n * Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS\n * runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for\n * 90 days in Safari: ITP caps JavaScript-set cookies on a fbclid-decorated landing page to 24\n * hours, and a document response is never classified as CNAME/IP cloaking (it is the reference the\n * browser measures cloaking against).\n *\n * Register it as a global request middleware:\n * ```ts\n * // start.ts\n * import { createStart } from '@tanstack/react-start'\n * import { clickIdMiddleware } from '@shware/analytics/tanstack'\n * export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))\n * ```\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\nexport function createClickIdMiddleware(options: ClickIdMiddlewareOptions = {}) {\n const { cacheControl = 'private, no-store' } = options;\n\n return createMiddleware({ type: 'request' }).server(async ({ request, next, handlerType }) => {\n const result = await next();\n\n // Skip serverFn RPC responses. 'router' covers SSR document requests *and* custom server\n // routes (API endpoints) — those also get cookies when the URL carries a click id.\n if (handlerType !== 'router') return result;\n if (options.shouldPersist && !options.shouldPersist(request)) return result;\n\n const { cookies } = resolveClickIdCookies({\n url: request.url,\n cookieHeader: request.headers.get('cookie'),\n domain: options.domain,\n secure: options.secure,\n subdomainIndex: options.subdomainIndex,\n refresh: options.refresh ?? true,\n });\n\n if (cookies.length > 0) {\n for (const header of toSetCookieHeaders(cookies)) {\n result.response.headers.append('set-cookie', header);\n }\n if (cacheControl !== false) {\n result.response.headers.set('cache-control', cacheControl);\n }\n }\n\n return result;\n });\n}\n\n/** Ready-to-register middleware with default options. */\nexport const clickIdMiddleware = createClickIdMiddleware();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,wBAAwB,UAAoC,CAAC,GAAG;CAC9E,MAAM,EAAE,eAAe,wBAAwB;CAE/C,QAAA,GAAOA,sBAAAA,iBAAAA,CAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,SAAS,MAAM,kBAAkB;EAC5F,MAAM,SAAS,MAAM,KAAK;EAI1B,IAAI,gBAAgB,UAAU,OAAO;EACrC,IAAI,QAAQ,iBAAiB,CAAC,QAAQ,cAAc,OAAO,GAAG,OAAO;EAErE,MAAM,EAAE,YAAYC,uBAAAA,sBAAsB;GACxC,KAAK,QAAQ;GACb,cAAc,QAAQ,QAAQ,IAAI,QAAQ;GAC1C,QAAQ,QAAQ;GAChB,QAAQ,QAAQ;GAChB,gBAAgB,QAAQ;GACxB,SAAS,QAAQ,WAAW;EAC9B,CAAC;EAED,IAAI,QAAQ,SAAS,GAAG;GACtB,KAAK,MAAM,UAAUC,uBAAAA,mBAAmB,OAAO,GAC7C,OAAO,SAAS,QAAQ,OAAO,cAAc,MAAM;GAErD,IAAI,iBAAiB,OACnB,OAAO,SAAS,QAAQ,IAAI,iBAAiB,YAAY;EAE7D;EAEA,OAAO;CACT,CAAC;AACH;;AAGA,MAAa,oBAAoB,wBAAwB"}
|
|
@@ -27,7 +27,8 @@ interface ClickIdMiddlewareOptions {
|
|
|
27
27
|
shouldPersist?: (request: Request) => boolean;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `
|
|
30
|
+
* TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`,
|
|
31
|
+
* `_rdt_cid`, `_uetmsclkid`) on the
|
|
31
32
|
* document response.
|
|
32
33
|
*
|
|
33
34
|
* Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.cts","names":[],"sources":["../../src/tanstack/middleware.ts"],"mappings":";UAGiB;;EAEf;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA;;;;;EAKA,iBAAiB,SAAS
|
|
1
|
+
{"version":3,"file":"middleware.d.cts","names":[],"sources":["../../src/tanstack/middleware.ts"],"mappings":";UAGiB;;EAEf;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA;;;;;EAKA,iBAAiB,SAAS;;;;;;;;;;;;;;;;;;;;;;;iBAwBZ,wBAAwB,UAAS,2DAA6B;;cAkCjE,mDAAiB"}
|
|
@@ -27,7 +27,8 @@ interface ClickIdMiddlewareOptions {
|
|
|
27
27
|
shouldPersist?: (request: Request) => boolean;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `
|
|
30
|
+
* TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`,
|
|
31
|
+
* `_rdt_cid`, `_uetmsclkid`) on the
|
|
31
32
|
* document response.
|
|
32
33
|
*
|
|
33
34
|
* Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/tanstack/middleware.ts"],"mappings":";UAGiB;;EAEf;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA;;;;;EAKA,iBAAiB,SAAS
|
|
1
|
+
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/tanstack/middleware.ts"],"mappings":";UAGiB;;EAEf;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA;;;;;EAKA,iBAAiB,SAAS;;;;;;;;;;;;;;;;;;;;;;;iBAwBZ,wBAAwB,UAAS,2DAA6B;;cAkCjE,mDAAiB"}
|
|
@@ -2,7 +2,8 @@ import { resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.mjs
|
|
|
2
2
|
import { createMiddleware } from "@tanstack/react-start";
|
|
3
3
|
//#region src/tanstack/middleware.ts
|
|
4
4
|
/**
|
|
5
|
-
* TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `
|
|
5
|
+
* TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`,
|
|
6
|
+
* `_rdt_cid`, `_uetmsclkid`) on the
|
|
6
7
|
* document response.
|
|
7
8
|
*
|
|
8
9
|
* Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /** `Domain` attribute for the cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (com=0, example.com=1, www.example.com=2). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` on every request as an ITP self-heal (restores the long-lived\n * HTTP cookie if the Meta Pixel's `document.cookie` write re-capped it to 24h in Safari). On by\n * default. Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page\n * response carrying an `_fbc`, defeating CDN caching of those pages; set false to strictly follow\n * Meta's conditional-write rule and keep them cacheable. See {@link resolveClickIdCookies}.\n */\n refresh?: boolean;\n /**\n * Override the `Cache-Control` of a response we attach cookies to (default `private, no-store`).\n * A per-user `Set-Cookie` must never end up on a shared-cache entry, or one visitor's `_fbc` would\n * be served to everyone. Only set this false if you guarantee these responses are never cached.\n */\n cacheControl?: string | false;\n /**\n * Consent gate. Return false to skip setting cookies for this request (e.g. before the visitor has\n * granted consent where required). Runs per request with the incoming `Request`.\n */\n shouldPersist?: (request: Request) => boolean;\n}\n\n/**\n * TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_rdt_cid`) on the\n * document response.\n *\n * Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS\n * runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for\n * 90 days in Safari: ITP caps JavaScript-set cookies on a fbclid-decorated landing page to 24\n * hours, and a document response is never classified as CNAME/IP cloaking (it is the reference the\n * browser measures cloaking against).\n *\n * Register it as a global request middleware:\n * ```ts\n * // start.ts\n * import { createStart } from '@tanstack/react-start'\n * import { clickIdMiddleware } from '@shware/analytics/tanstack'\n * export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))\n * ```\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\nexport function createClickIdMiddleware(options: ClickIdMiddlewareOptions = {}) {\n const { cacheControl = 'private, no-store' } = options;\n\n return createMiddleware({ type: 'request' }).server(async ({ request, next, handlerType }) => {\n const result = await next();\n\n // Skip serverFn RPC responses. 'router' covers SSR document requests *and* custom server\n // routes (API endpoints) — those also get cookies when the URL carries a click id.\n if (handlerType !== 'router') return result;\n if (options.shouldPersist && !options.shouldPersist(request)) return result;\n\n const { cookies } = resolveClickIdCookies({\n url: request.url,\n cookieHeader: request.headers.get('cookie'),\n domain: options.domain,\n secure: options.secure,\n subdomainIndex: options.subdomainIndex,\n refresh: options.refresh ?? true,\n });\n\n if (cookies.length > 0) {\n for (const header of toSetCookieHeaders(cookies)) {\n result.response.headers.append('set-cookie', header);\n }\n if (cacheControl !== false) {\n result.response.headers.set('cache-control', cacheControl);\n }\n }\n\n return result;\n });\n}\n\n/** Ready-to-register middleware with default options. */\nexport const clickIdMiddleware = createClickIdMiddleware();\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/tanstack/middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start';\nimport { resolveClickIdCookies, toSetCookieHeaders } from '../click-id/index';\n\nexport interface ClickIdMiddlewareOptions {\n /** `Domain` attribute for the cookies, e.g. `.shware.io`. Omit for a host-only cookie. */\n domain?: string;\n /** `Secure` attribute, default true. Set false only for local http testing. */\n secure?: boolean;\n /** subdomainIndex for a freshly built `_fbc` (com=0, example.com=1, www.example.com=2). Default 1. */\n subdomainIndex?: number;\n /**\n * Re-issue a still-valid `_fbc` on every request as an ITP self-heal (restores the long-lived\n * HTTP cookie if the Meta Pixel's `document.cookie` write re-capped it to 24h in Safari). On by\n * default. Note it attaches a per-user `Set-Cookie` — and thus `no-store` — to every page\n * response carrying an `_fbc`, defeating CDN caching of those pages; set false to strictly follow\n * Meta's conditional-write rule and keep them cacheable. See {@link resolveClickIdCookies}.\n */\n refresh?: boolean;\n /**\n * Override the `Cache-Control` of a response we attach cookies to (default `private, no-store`).\n * A per-user `Set-Cookie` must never end up on a shared-cache entry, or one visitor's `_fbc` would\n * be served to everyone. Only set this false if you guarantee these responses are never cached.\n */\n cacheControl?: string | false;\n /**\n * Consent gate. Return false to skip setting cookies for this request (e.g. before the visitor has\n * granted consent where required). Runs per request with the incoming `Request`.\n */\n shouldPersist?: (request: Request) => boolean;\n}\n\n/**\n * TanStack Start request middleware that persists ad click-id cookies (`_fbc`, `_gcl_aw`/`_gcl_gb`,\n * `_rdt_cid`, `_uetmsclkid`) on the\n * document response.\n *\n * Setting `_fbc` here — on the top document via an HTTP `Set-Cookie` header, before any client JS\n * runs — is what Meta officially recommends and the only reliable way to keep the cookie alive for\n * 90 days in Safari: ITP caps JavaScript-set cookies on a fbclid-decorated landing page to 24\n * hours, and a document response is never classified as CNAME/IP cloaking (it is the reference the\n * browser measures cloaking against).\n *\n * Register it as a global request middleware:\n * ```ts\n * // start.ts\n * import { createStart } from '@tanstack/react-start'\n * import { clickIdMiddleware } from '@shware/analytics/tanstack'\n * export const startInstance = createStart(() => ({ requestMiddleware: [clickIdMiddleware] }))\n * ```\n *\n * reference: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc\n */\nexport function createClickIdMiddleware(options: ClickIdMiddlewareOptions = {}) {\n const { cacheControl = 'private, no-store' } = options;\n\n return createMiddleware({ type: 'request' }).server(async ({ request, next, handlerType }) => {\n const result = await next();\n\n // Skip serverFn RPC responses. 'router' covers SSR document requests *and* custom server\n // routes (API endpoints) — those also get cookies when the URL carries a click id.\n if (handlerType !== 'router') return result;\n if (options.shouldPersist && !options.shouldPersist(request)) return result;\n\n const { cookies } = resolveClickIdCookies({\n url: request.url,\n cookieHeader: request.headers.get('cookie'),\n domain: options.domain,\n secure: options.secure,\n subdomainIndex: options.subdomainIndex,\n refresh: options.refresh ?? true,\n });\n\n if (cookies.length > 0) {\n for (const header of toSetCookieHeaders(cookies)) {\n result.response.headers.append('set-cookie', header);\n }\n if (cacheControl !== false) {\n result.response.headers.set('cache-control', cacheControl);\n }\n }\n\n return result;\n });\n}\n\n/** Ready-to-register middleware with default options. */\nexport const clickIdMiddleware = createClickIdMiddleware();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,wBAAwB,UAAoC,CAAC,GAAG;CAC9E,MAAM,EAAE,eAAe,wBAAwB;CAE/C,OAAO,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,SAAS,MAAM,kBAAkB;EAC5F,MAAM,SAAS,MAAM,KAAK;EAI1B,IAAI,gBAAgB,UAAU,OAAO;EACrC,IAAI,QAAQ,iBAAiB,CAAC,QAAQ,cAAc,OAAO,GAAG,OAAO;EAErE,MAAM,EAAE,YAAY,sBAAsB;GACxC,KAAK,QAAQ;GACb,cAAc,QAAQ,QAAQ,IAAI,QAAQ;GAC1C,QAAQ,QAAQ;GAChB,QAAQ,QAAQ;GAChB,gBAAgB,QAAQ;GACxB,SAAS,QAAQ,WAAW;EAC9B,CAAC;EAED,IAAI,QAAQ,SAAS,GAAG;GACtB,KAAK,MAAM,UAAU,mBAAmB,OAAO,GAC7C,OAAO,SAAS,QAAQ,OAAO,cAAc,MAAM;GAErD,IAAI,iBAAiB,OACnB,OAAO,SAAS,QAAQ,IAAI,iBAAiB,YAAY;EAE7D;EAEA,OAAO;CACT,CAAC;AACH;;AAGA,MAAa,oBAAoB,wBAAwB"}
|
|
@@ -4,13 +4,18 @@ const require_third_parties_meta_pixel = require("./meta-pixel.cjs");
|
|
|
4
4
|
const require_third_parties_linkedin_insight_tag = require("./linkedin-insight-tag.cjs");
|
|
5
5
|
const require_third_parties_openai_pixel = require("./openai-pixel.cjs");
|
|
6
6
|
const require_third_parties_reddit_pixel = require("./reddit-pixel.cjs");
|
|
7
|
+
const require_third_parties_microsoft_uet = require("./microsoft-uet.cjs");
|
|
7
8
|
exports.sendFBEvent = require_third_parties_meta_pixel.sendFBEvent;
|
|
8
9
|
exports.sendGAEvent = require_third_parties_google_analytics.sendGAEvent;
|
|
9
10
|
exports.sendLinkedinEvent = require_third_parties_linkedin_insight_tag.sendLinkedinEvent;
|
|
10
11
|
exports.sendOpenAIEvent = require_third_parties_openai_pixel.sendOpenAIEvent;
|
|
11
12
|
exports.sendRedditEvent = require_third_parties_reddit_pixel.sendRedditEvent;
|
|
13
|
+
exports.sendUETEvent = require_third_parties_microsoft_uet.sendUETEvent;
|
|
14
|
+
exports.sendUETIdSync = require_third_parties_microsoft_uet.sendUETIdSync;
|
|
12
15
|
exports.setFBUser = require_third_parties_meta_pixel.setFBUser;
|
|
13
16
|
exports.setGAUser = require_third_parties_google_analytics.setGAUser;
|
|
14
17
|
exports.setLinkedinUser = require_third_parties_linkedin_insight_tag.setLinkedinUser;
|
|
15
18
|
exports.setOpenAIUser = require_third_parties_openai_pixel.setOpenAIUser;
|
|
16
19
|
exports.setRedditUser = require_third_parties_reddit_pixel.setRedditUser;
|
|
20
|
+
exports.setUETConsent = require_third_parties_microsoft_uet.setUETConsent;
|
|
21
|
+
exports.setUETUser = require_third_parties_microsoft_uet.setUETUser;
|
|
@@ -3,4 +3,6 @@ import { sendFBEvent, setFBUser } from "./meta-pixel.cjs";
|
|
|
3
3
|
import { LinkedinConversionConfig, sendLinkedinEvent, setLinkedinUser } from "./linkedin-insight-tag.cjs";
|
|
4
4
|
import { sendOpenAIEvent, setOpenAIUser } from "./openai-pixel.cjs";
|
|
5
5
|
import { sendRedditEvent, setRedditUser } from "./reddit-pixel.cjs";
|
|
6
|
-
|
|
6
|
+
import { UETConsent, UETEventParams, UETItem, UETPageType, UETPid } from "../track/uetq.cjs";
|
|
7
|
+
import { sendUETEvent, sendUETIdSync, setUETConsent, setUETUser } from "./microsoft-uet.cjs";
|
|
8
|
+
export { type LinkedinConversionConfig, type UETConsent, type UETEventParams, type UETItem, type UETPageType, type UETPid, sendFBEvent, sendGAEvent, sendLinkedinEvent, sendOpenAIEvent, sendRedditEvent, sendUETEvent, sendUETIdSync, setFBUser, setGAUser, setLinkedinUser, setOpenAIUser, setRedditUser, setUETConsent, setUETUser };
|
|
@@ -3,4 +3,6 @@ import { sendFBEvent, setFBUser } from "./meta-pixel.mjs";
|
|
|
3
3
|
import { LinkedinConversionConfig, sendLinkedinEvent, setLinkedinUser } from "./linkedin-insight-tag.mjs";
|
|
4
4
|
import { sendOpenAIEvent, setOpenAIUser } from "./openai-pixel.mjs";
|
|
5
5
|
import { sendRedditEvent, setRedditUser } from "./reddit-pixel.mjs";
|
|
6
|
-
|
|
6
|
+
import { UETConsent, UETEventParams, UETItem, UETPageType, UETPid } from "../track/uetq.mjs";
|
|
7
|
+
import { sendUETEvent, sendUETIdSync, setUETConsent, setUETUser } from "./microsoft-uet.mjs";
|
|
8
|
+
export { type LinkedinConversionConfig, type UETConsent, type UETEventParams, type UETItem, type UETPageType, type UETPid, sendFBEvent, sendGAEvent, sendLinkedinEvent, sendOpenAIEvent, sendRedditEvent, sendUETEvent, sendUETIdSync, setFBUser, setGAUser, setLinkedinUser, setOpenAIUser, setRedditUser, setUETConsent, setUETUser };
|
|
@@ -3,4 +3,5 @@ import { sendFBEvent, setFBUser } from "./meta-pixel.mjs";
|
|
|
3
3
|
import { sendLinkedinEvent, setLinkedinUser } from "./linkedin-insight-tag.mjs";
|
|
4
4
|
import { sendOpenAIEvent, setOpenAIUser } from "./openai-pixel.mjs";
|
|
5
5
|
import { sendRedditEvent, setRedditUser } from "./reddit-pixel.mjs";
|
|
6
|
-
|
|
6
|
+
import { sendUETEvent, sendUETIdSync, setUETConsent, setUETUser } from "./microsoft-uet.mjs";
|
|
7
|
+
export { sendFBEvent, sendGAEvent, sendLinkedinEvent, sendOpenAIEvent, sendRedditEvent, sendUETEvent, sendUETIdSync, setFBUser, setGAUser, setLinkedinUser, setOpenAIUser, setRedditUser, setUETConsent, setUETUser };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_utils_field = require("../utils/field.cjs");
|
|
3
|
+
const require_track_uetq = require("../track/uetq.cjs");
|
|
4
|
+
const require_utils_sha256 = require("../utils/sha256.cjs");
|
|
5
|
+
//#region src/third-parties/microsoft-uet.ts
|
|
6
|
+
const metrics = [
|
|
7
|
+
"CLS",
|
|
8
|
+
"FCP",
|
|
9
|
+
"FID",
|
|
10
|
+
"INP",
|
|
11
|
+
"LCP",
|
|
12
|
+
"TTFB"
|
|
13
|
+
];
|
|
14
|
+
/** Drop `undefined` fields: bat.js validates every key it receives, and `undefined` is a value. */
|
|
15
|
+
function clean(value) {
|
|
16
|
+
return JSON.parse(JSON.stringify(value));
|
|
17
|
+
}
|
|
18
|
+
function isLocal() {
|
|
19
|
+
return window.location.host.includes("127.0.0.1") || window.location.host.includes("localhost");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Forward an internal track event to the Microsoft Advertising UET tag.
|
|
23
|
+
*
|
|
24
|
+
* `eventId` is reused as UET's `event_id` so the browser event deduplicates against the same
|
|
25
|
+
* conversion sent through the Conversions API (`sendMicrosoftEvents`). The action name is the
|
|
26
|
+
* internal name on both channels — see `mapUETEvent`.
|
|
27
|
+
*
|
|
28
|
+
* `page_view` is not forwarded: bat.js turns that action into its page-load beacon, which the
|
|
29
|
+
* snippet already fires on load and, with `enableAutoSpaTracking: true` (the setting in
|
|
30
|
+
* Microsoft's generated snippet), on every history change — forwarding the SDK's own page views
|
|
31
|
+
* would report each page twice. The tag owns page loads; this tracker owns everything else.
|
|
32
|
+
*/
|
|
33
|
+
function sendUETEvent(name, properties, eventId) {
|
|
34
|
+
if (typeof window === "undefined" || !window.uetq) {
|
|
35
|
+
console.warn("uetq has not been initialized");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (metrics.includes(name) || name === "page_view") return;
|
|
39
|
+
if (isLocal()) return;
|
|
40
|
+
const [action, params] = require_track_uetq.mapUETEvent(name, properties, eventId);
|
|
41
|
+
window.uetq.push("event", action, clean(params));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Enhanced conversions: hand the tag the visitor's email and phone so Microsoft can match a
|
|
45
|
+
* conversion to an ad click without a third-party cookie. A `set` applies to every event fired
|
|
46
|
+
* from the page afterwards, `pageLoad` included, and the tag fires a `pid` beacon on its own
|
|
47
|
+
* when the page load already went out. Values are sent raw — the tag normalizes and SHA-256
|
|
48
|
+
* hashes them in the browser (bat.js `validatePid`) before anything leaves the page.
|
|
49
|
+
*
|
|
50
|
+
* No tag id parameter: unlike the Meta/Reddit pixels, UET is bound to its tag by the snippet
|
|
51
|
+
* and `set` addresses whichever tag drains `uetq`.
|
|
52
|
+
*/
|
|
53
|
+
function setUETUser() {
|
|
54
|
+
return ({ user_data }) => {
|
|
55
|
+
if (typeof window === "undefined" || !window.uetq) {
|
|
56
|
+
console.warn("uetq has not been initialized");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const em = require_utils_field.getFirst(user_data?.email);
|
|
60
|
+
const ph = require_utils_field.getFirst(user_data?.phone_number);
|
|
61
|
+
if (!em && !ph) return;
|
|
62
|
+
window.uetq.push("set", { pid: clean({
|
|
63
|
+
em,
|
|
64
|
+
ph
|
|
65
|
+
}) });
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* UET consent mode. Push `default` before the tag loads (the tag processes `consent` early, ahead
|
|
70
|
+
* of the queued events) and `update` when the visitor decides. With `ad_storage: 'denied'` the
|
|
71
|
+
* tag neither writes its cookies nor reads the `msclkid` from the URL.
|
|
72
|
+
* https://help.ads.microsoft.com/#apex/ads/en/60119/1
|
|
73
|
+
*/
|
|
74
|
+
function setUETConsent(mode, consent) {
|
|
75
|
+
if (typeof window === "undefined") return;
|
|
76
|
+
window.uetq ??= [];
|
|
77
|
+
window.uetq.push("consent", mode, consent);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The Conversions API's client-side ID Sync pixel: ties the visitor id sent server-side as
|
|
81
|
+
* `anonymousId` to Microsoft's own identifier, so server events can be attributed across
|
|
82
|
+
* contexts (view-through, audiences). Microsoft requires it for remarketing built from CAPI
|
|
83
|
+
* events and recommends it for measurement quality; fire it at least once per session, ideally
|
|
84
|
+
* on the first page view. A pixel rather than a fetch on purpose: Microsoft must observe the
|
|
85
|
+
* browser context of the sync, which a server-side call cannot carry.
|
|
86
|
+
*
|
|
87
|
+
* `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET
|
|
88
|
+
* tag id. `visitorId` is the SDK visitor id (`getVisitor().id`), which is what
|
|
89
|
+
* `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.
|
|
90
|
+
* `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender
|
|
91
|
+
* hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the
|
|
92
|
+
* pixel fires once the digest resolves; where SubtleCrypto is unavailable (an insecure context)
|
|
93
|
+
* the pixel goes out without `UID` rather than with a raw id.
|
|
94
|
+
* https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#id-sync-and-why-it-matters
|
|
95
|
+
*/
|
|
96
|
+
function sendUETIdSync(options) {
|
|
97
|
+
if (typeof window === "undefined" || typeof Image === "undefined") return;
|
|
98
|
+
if (isLocal()) return;
|
|
99
|
+
const fire = (uid) => {
|
|
100
|
+
const params = new URLSearchParams({
|
|
101
|
+
Red3: `BACID_${options.customerId}`,
|
|
102
|
+
VID: options.visitorId
|
|
103
|
+
});
|
|
104
|
+
if (uid) params.set("UID", uid);
|
|
105
|
+
new Image().src = `https://c.bing.com/c.gif?${params.toString()}`;
|
|
106
|
+
};
|
|
107
|
+
if (!options.userId) {
|
|
108
|
+
fire();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
require_utils_sha256.sha256(options.userId).then(fire, () => fire());
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
exports.sendUETEvent = sendUETEvent;
|
|
115
|
+
exports.sendUETIdSync = sendUETIdSync;
|
|
116
|
+
exports.setUETConsent = setUETConsent;
|
|
117
|
+
exports.setUETUser = setUETUser;
|
|
118
|
+
|
|
119
|
+
//# sourceMappingURL=microsoft-uet.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-uet.cjs","names":["mapUETEvent","getFirst"],"sources":["../../src/third-parties/microsoft-uet.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { type UETConsent, type UETQ, mapUETEvent } from '../track/uetq';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /**\n * Defined by the UET snippet before `bat.js` loads (as a plain array), so pushes made early\n * are queued rather than lost. Undefined only where the snippet was never installed.\n */\n uetq?: UETQ;\n }\n}\n\nconst metrics = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'];\n\n/** Drop `undefined` fields: bat.js validates every key it receives, and `undefined` is a value. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction isLocal(): boolean {\n return window.location.host.includes('127.0.0.1') || window.location.host.includes('localhost');\n}\n\n/**\n * Forward an internal track event to the Microsoft Advertising UET tag.\n *\n * `eventId` is reused as UET's `event_id` so the browser event deduplicates against the same\n * conversion sent through the Conversions API (`sendMicrosoftEvents`). The action name is the\n * internal name on both channels — see `mapUETEvent`.\n *\n * `page_view` is not forwarded: bat.js turns that action into its page-load beacon, which the\n * snippet already fires on load and, with `enableAutoSpaTracking: true` (the setting in\n * Microsoft's generated snippet), on every history change — forwarding the SDK's own page views\n * would report each page twice. The tag owns page loads; this tracker owns everything else.\n */\nexport function sendUETEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n if (metrics.includes(name) || name === 'page_view') return;\n if (isLocal()) return;\n\n const [action, params] = mapUETEvent(name, properties, eventId);\n window.uetq.push('event', action, clean(params));\n}\n\n/**\n * Enhanced conversions: hand the tag the visitor's email and phone so Microsoft can match a\n * conversion to an ad click without a third-party cookie. A `set` applies to every event fired\n * from the page afterwards, `pageLoad` included, and the tag fires a `pid` beacon on its own\n * when the page load already went out. Values are sent raw — the tag normalizes and SHA-256\n * hashes them in the browser (bat.js `validatePid`) before anything leaves the page.\n *\n * No tag id parameter: unlike the Meta/Reddit pixels, UET is bound to its tag by the snippet\n * and `set` addresses whichever tag drains `uetq`.\n */\nexport function setUETUser() {\n return ({ user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.uetq) {\n console.warn('uetq has not been initialized');\n return;\n }\n\n const em = getFirst(user_data?.email);\n const ph = getFirst(user_data?.phone_number);\n if (!em && !ph) return;\n\n window.uetq.push('set', { pid: clean({ em, ph }) });\n };\n}\n\n/**\n * UET consent mode. Push `default` before the tag loads (the tag processes `consent` early, ahead\n * of the queued events) and `update` when the visitor decides. With `ad_storage: 'denied'` the\n * tag neither writes its cookies nor reads the `msclkid` from the URL.\n * https://help.ads.microsoft.com/#apex/ads/en/60119/1\n */\nexport function setUETConsent(mode: 'default' | 'update', consent: UETConsent) {\n if (typeof window === 'undefined') return;\n window.uetq ??= [] as unknown as UETQ;\n window.uetq.push('consent', mode, consent);\n}\n\n/**\n * The Conversions API's client-side ID Sync pixel: ties the visitor id sent server-side as\n * `anonymousId` to Microsoft's own identifier, so server events can be attributed across\n * contexts (view-through, audiences). Microsoft requires it for remarketing built from CAPI\n * events and recommends it for measurement quality; fire it at least once per session, ideally\n * on the first page view. A pixel rather than a fetch on purpose: Microsoft must observe the\n * browser context of the sync, which a server-side call cannot carry.\n *\n * `customerId` is the Microsoft Advertising customer id (`cid` in the UI's URLs) — not the UET\n * tag id. `visitorId` is the SDK visitor id (`getVisitor().id`), which is what\n * `sendMicrosoftEvents` sends as `anonymousId`; Microsoft requires the two to be equal.\n * `userId` is the raw signed-in user id: it is SHA-256 hashed here, exactly as the server sender\n * hashes it into `externalId`, so the pair matches as well. Hashing is asynchronous, so the\n * pixel fires once the digest resolves; where SubtleCrypto is unavailable (an insecure context)\n * the pixel goes out without `UID` rather than with a raw id.\n * https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#id-sync-and-why-it-matters\n */\nexport function sendUETIdSync(options: {\n customerId: string | number;\n visitorId: string;\n /** The signed-in user's id, raw; hashed before it leaves the page. */\n userId?: string;\n}) {\n if (typeof window === 'undefined' || typeof Image === 'undefined') return;\n if (isLocal()) return;\n\n const fire = (uid?: string) => {\n const params = new URLSearchParams({\n Red3: `BACID_${options.customerId}`,\n VID: options.visitorId,\n });\n if (uid) params.set('UID', uid);\n new Image().src = `https://c.bing.com/c.gif?${params.toString()}`;\n };\n\n if (!options.userId) {\n fire();\n return;\n }\n sha256(options.userId).then(fire, () => fire());\n}\n"],"mappings":";;;;;AAgBA,MAAM,UAAU;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;AAAM;;AAI1D,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;AAEA,SAAS,UAAmB;CAC1B,OAAO,OAAO,SAAS,KAAK,SAAS,WAAW,KAAK,OAAO,SAAS,KAAK,SAAS,WAAW;AAChG;;;;;;;;;;;;;AAcA,SAAgB,aACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,QAAQ,SAAS,IAAI,KAAK,SAAS,aAAa;CACpD,IAAI,QAAQ,GAAG;CAEf,MAAM,CAAC,QAAQ,UAAUA,mBAAAA,YAAY,MAAM,YAAY,OAAO;CAC9D,OAAO,KAAK,KAAK,SAAS,QAAQ,MAAM,MAAM,CAAC;AACjD;;;;;;;;;;;AAYA,SAAgB,aAAa;CAC3B,QAAQ,EAAE,gBAAkC;EAC1C,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,KAAKC,oBAAAA,SAAS,WAAW,KAAK;EACpC,MAAM,KAAKA,oBAAAA,SAAS,WAAW,YAAY;EAC3C,IAAI,CAAC,MAAM,CAAC,IAAI;EAEhB,OAAO,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM;GAAE;GAAI;EAAG,CAAC,EAAE,CAAC;CACpD;AACF;;;;;;;AAQA,SAAgB,cAAc,MAA4B,SAAqB;CAC7E,IAAI,OAAO,WAAW,aAAa;CACnC,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,KAAK,WAAW,MAAM,OAAO;AAC3C;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cAAc,SAK3B;CACD,IAAI,OAAO,WAAW,eAAe,OAAO,UAAU,aAAa;CACnE,IAAI,QAAQ,GAAG;CAEf,MAAM,QAAQ,QAAiB;EAC7B,MAAM,SAAS,IAAI,gBAAgB;GACjC,MAAM,SAAS,QAAQ;GACvB,KAAK,QAAQ;EACf,CAAC;EACD,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG;EAC9B,IAAI,MAAM,CAAC,CAAC,MAAM,4BAA4B,OAAO,SAAS;CAChE;CAEA,IAAI,CAAC,QAAQ,QAAQ;EACnB,KAAK;EACL;CACF;CACA,qBAAA,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;AAChD"}
|