@shware/analytics 8.2.0 → 8.5.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 +78 -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/hooks/use-web-analytics.cjs +11 -5
- package/dist/hooks/use-web-analytics.cjs.map +1 -1
- package/dist/hooks/use-web-analytics.d.cts +4 -1
- package/dist/hooks/use-web-analytics.d.cts.map +1 -1
- package/dist/hooks/use-web-analytics.d.mts +4 -1
- package/dist/hooks/use-web-analytics.d.mts.map +1 -1
- package/dist/hooks/use-web-analytics.mjs +11 -5
- package/dist/hooks/use-web-analytics.mjs.map +1 -1
- package/dist/next/index.cjs +38 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +3 -1
- package/dist/next/index.d.cts.map +1 -1
- package/dist/next/index.d.mts +3 -1
- package/dist/next/index.d.mts.map +1 -1
- package/dist/next/index.mjs +39 -3
- package/dist/next/index.mjs.map +1 -1
- package/dist/react-router/index.cjs +25 -3
- package/dist/react-router/index.cjs.map +1 -1
- package/dist/react-router/index.d.cts +3 -1
- package/dist/react-router/index.d.cts.map +1 -1
- package/dist/react-router/index.d.mts +3 -1
- package/dist/react-router/index.d.mts.map +1 -1
- package/dist/react-router/index.mjs +25 -3
- package/dist/react-router/index.mjs.map +1 -1
- package/dist/schema/index.cjs +1 -0
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts +4 -0
- package/dist/schema/index.d.cts.map +1 -1
- package/dist/schema/index.d.mts +4 -0
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/schema/index.mjs +1 -0
- package/dist/schema/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 +124 -0
- package/dist/server/microsoft-conversions-api.d.cts.map +1 -0
- package/dist/server/microsoft-conversions-api.d.mts +124 -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/analytics.cjs +25 -3
- package/dist/tanstack/analytics.cjs.map +1 -1
- package/dist/tanstack/analytics.d.cts +3 -1
- package/dist/tanstack/analytics.d.cts.map +1 -1
- package/dist/tanstack/analytics.d.mts +3 -1
- package/dist/tanstack/analytics.d.mts.map +1 -1
- package/dist/tanstack/analytics.mjs +25 -3
- package/dist/tanstack/analytics.mjs.map +1 -1
- 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/types.d.cts +7 -0
- package/dist/track/types.d.cts.map +1 -1
- package/dist/track/types.d.mts +7 -0
- package/dist/track/types.d.mts.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 +26 -2
- 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 +27 -3
- package/dist/web/index.mjs.map +1 -1
- package/dist/web/page-key.cjs +53 -0
- package/dist/web/page-key.cjs.map +1 -0
- package/dist/web/page-key.d.cts +24 -0
- package/dist/web/page-key.d.cts.map +1 -0
- package/dist/web/page-key.d.mts +24 -0
- package/dist/web/page-key.d.mts.map +1 -0
- package/dist/web/page-key.mjs +52 -0
- package/dist/web/page-key.mjs.map +1 -0
- package/package.json +1 -1
package/dist/server/index.d.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, ParsedFbc, ParsedGcl, RDT_CID_COOKIE, ResolveClickIdCookiesInput, ResolveClickIdCookiesResult, formatFbc, formatGcl, parseFbc, parseGcl, resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.mjs";
|
|
1
|
+
import { FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, ParsedFbc, ParsedGcl, RDT_CID_COOKIE, ResolveClickIdCookiesInput, ResolveClickIdCookiesResult, UET_MSCLKID_COOKIE, formatFbc, formatGcl, formatMsclkid, formatUetMsclkid, parseFbc, parseGcl, parseUetMsclkid, resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.mjs";
|
|
2
2
|
import { EventActionSource } from "./action-source.mjs";
|
|
3
3
|
import { DataManagerEvent, DataManagerResponse, GoogleAdsAuth, GoogleAdsConsent, GoogleAdsConversionConfig, GoogleAdsConversionsOptions, getDataManagerEvent, normalizeEmail, sendEvents } from "./google-data-manager.mjs";
|
|
4
4
|
import { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendEvents as sendEvents$2 } from "./meta-capi.mjs";
|
|
5
|
-
import { sendEvents as sendEvents$
|
|
5
|
+
import { sendEvents as sendEvents$5 } from "./reddit-conversions-api.mjs";
|
|
6
6
|
import { LinkedinConversionConfig, sendEvents as sendEvents$1 } from "./linkedin-conversions-api.mjs";
|
|
7
|
-
import { sendEvents as sendEvents$
|
|
8
|
-
|
|
7
|
+
import { sendEvents as sendEvents$4 } from "./openai-conversions-api.mjs";
|
|
8
|
+
import { MicrosoftConversionsOptions, MicrosoftConversionsResponse, MicrosoftCustomData, MicrosoftEvent, MicrosoftEventOptions, MicrosoftUserData, MicrosoftValidationDetail, getServerEvent, normalizeEmail as normalizeEmail$1, sendEvents as sendEvents$3 } from "./microsoft-conversions-api.mjs";
|
|
9
|
+
export { type CapiEvent, type DataManagerEvent, type DataManagerResponse, type EventActionSource, FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, type GoogleAdsAuth, type GoogleAdsConsent, type GoogleAdsConversionConfig, type GoogleAdsConversionsOptions, type LinkedinConversionConfig, type MetaConversionsOptions, type MetaConversionsResponse, type MicrosoftConversionsOptions, type MicrosoftConversionsResponse, type MicrosoftCustomData, type MicrosoftEvent, type MicrosoftEventOptions, type MicrosoftUserData, type MicrosoftValidationDetail, type ParsedFbc, type ParsedGcl, RDT_CID_COOKIE, type ResolveClickIdCookiesInput, type ResolveClickIdCookiesResult, UET_MSCLKID_COOKIE, formatFbc, formatGcl, formatMsclkid, formatUetMsclkid, getCapiEvent, getDataManagerEvent, getServerEvent as getMicrosoftEvent, normalizeEmail, normalizeEmail$1 as normalizeMicrosoftEmail, parseFbc, parseGcl, parseUetMsclkid, resolveClickIdCookies, sendEvents as sendGoogleAdsEvents, sendEvents$1 as sendLinkedinEvents, sendEvents$2 as sendMetaEvents, sendEvents$3 as sendMicrosoftEvents, sendEvents$4 as sendOpenAIEvents, sendEvents$5 as sendRedditEvents, toSetCookieHeaders };
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, RDT_CID_COOKIE, formatFbc, formatGcl, parseFbc, parseGcl, resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.mjs";
|
|
1
|
+
import { FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, RDT_CID_COOKIE, UET_MSCLKID_COOKIE, formatFbc, formatGcl, formatMsclkid, formatUetMsclkid, parseFbc, parseGcl, parseUetMsclkid, resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.mjs";
|
|
2
2
|
import { getDataManagerEvent, normalizeEmail, sendEvents } from "./google-data-manager.mjs";
|
|
3
3
|
import { getCapiEvent, sendEvents as sendEvents$2 } from "./meta-capi.mjs";
|
|
4
|
-
import { sendEvents as sendEvents$
|
|
4
|
+
import { sendEvents as sendEvents$5 } from "./reddit-conversions-api.mjs";
|
|
5
5
|
import { sendEvents as sendEvents$1 } from "./linkedin-conversions-api.mjs";
|
|
6
|
-
import { sendEvents as sendEvents$
|
|
7
|
-
|
|
6
|
+
import { sendEvents as sendEvents$4 } from "./openai-conversions-api.mjs";
|
|
7
|
+
import { getServerEvent, normalizeEmail as normalizeEmail$1, sendEvents as sendEvents$3 } from "./microsoft-conversions-api.mjs";
|
|
8
|
+
export { FBC_COOKIE, GCL_AW_COOKIE, GCL_GB_COOKIE, RDT_CID_COOKIE, UET_MSCLKID_COOKIE, formatFbc, formatGcl, formatMsclkid, formatUetMsclkid, getCapiEvent, getDataManagerEvent, getServerEvent as getMicrosoftEvent, normalizeEmail, normalizeEmail$1 as normalizeMicrosoftEmail, parseFbc, parseGcl, parseUetMsclkid, resolveClickIdCookies, sendEvents as sendGoogleAdsEvents, sendEvents$1 as sendLinkedinEvents, sendEvents$2 as sendMetaEvents, sendEvents$3 as sendMicrosoftEvents, sendEvents$4 as sendOpenAIEvents, sendEvents$5 as sendRedditEvents, toSetCookieHeaders };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_third_parties_ignored_events = require("../third-parties/ignored-events.cjs");
|
|
3
|
+
const require_click_id_index = require("../click-id/index.cjs");
|
|
4
|
+
const require_server_page_location = require("./page-location.cjs");
|
|
5
|
+
const require_track_uetq = require("../track/uetq.cjs");
|
|
6
|
+
let _shware_utils = require("@shware/utils");
|
|
7
|
+
let node_crypto = require("node:crypto");
|
|
8
|
+
//#region src/server/microsoft-conversions-api.ts
|
|
9
|
+
/**
|
|
10
|
+
* Microsoft Advertising Conversions API (CAPI) sender — the server-side counterpart of the UET
|
|
11
|
+
* tag, in the same shape as the other senders here: a pure per-event builder, a never-throws
|
|
12
|
+
* `sendEvents`, the token in a header and never in a URL or a log line.
|
|
13
|
+
*
|
|
14
|
+
* Microsoft's recommended deployment is hybrid: keep the UET JavaScript on the page and send the
|
|
15
|
+
* same conversions here too, deduplicated by (`tagId`, `eventName`, `eventId`). Both channels in
|
|
16
|
+
* this SDK are fed from the same `track()` call — the browser sends the internal name as the UET
|
|
17
|
+
* action with the event id as `event_id`, and this builder sends the same name as `eventName`
|
|
18
|
+
* with the same id — so the pair agrees by construction.
|
|
19
|
+
*
|
|
20
|
+
* The token comes from the UET tag's "Use Conversions API" setup step, or from the Campaign
|
|
21
|
+
* Management API's `UetTagAuthKey/Query`. Access is provisioned per customer account: an account
|
|
22
|
+
* whose tag setup has no "Use Conversions API" option has to ask Microsoft Advertising support.
|
|
23
|
+
*
|
|
24
|
+
* https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration
|
|
25
|
+
*/
|
|
26
|
+
const ENDPOINT = "https://capi.uet.microsoft.com/v1";
|
|
27
|
+
/** The API's hard limit per request. */
|
|
28
|
+
const MAX_EVENTS_PER_REQUEST = 1e3;
|
|
29
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
30
|
+
const sha256 = (value) => (0, node_crypto.createHash)("sha256").update(value).digest("hex");
|
|
31
|
+
/**
|
|
32
|
+
* Microsoft's email normalization: trim, drop the dots and any `+alias` from the user part —
|
|
33
|
+
* for EVERY domain, unlike Google's gmail-only rule — lowercase, SHA-256. A value that is
|
|
34
|
+
* already a SHA-256 hex digest passes through.
|
|
35
|
+
* https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration#hashed-identifiers
|
|
36
|
+
*/
|
|
37
|
+
function normalizeEmail(input) {
|
|
38
|
+
const email = input.trim().toLowerCase();
|
|
39
|
+
const at = email.lastIndexOf("@");
|
|
40
|
+
if (at === -1) return email;
|
|
41
|
+
return `${email.slice(0, at).replace(/\./g, "").split("+", 1)[0] ?? ""}@${email.slice(at + 1)}`;
|
|
42
|
+
}
|
|
43
|
+
function hashEmail(input) {
|
|
44
|
+
const value = input?.trim().toLowerCase();
|
|
45
|
+
if (!value) return void 0;
|
|
46
|
+
return SHA256.test(value) ? value : sha256(normalizeEmail(value));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Phones are E.164 by the schema (`+` then 11–15 digits, the range bat.js validates too); the
|
|
50
|
+
* separators bat.js tolerates are stripped, and anything else is dropped rather than guessed —
|
|
51
|
+
* a number with no country code hashed under an assumed one can never match.
|
|
52
|
+
*/
|
|
53
|
+
function hashPhone(input) {
|
|
54
|
+
const value = input?.trim().toLowerCase();
|
|
55
|
+
if (!value) return void 0;
|
|
56
|
+
if (SHA256.test(value)) return value;
|
|
57
|
+
const phone = value.replace(/[-() \t]/g, "");
|
|
58
|
+
return /^\+\d{11,15}$/.test(phone) ? sha256(phone) : void 0;
|
|
59
|
+
}
|
|
60
|
+
function getUserData(event, data) {
|
|
61
|
+
const { tags, platform, visitor_id } = event;
|
|
62
|
+
const email = Array.isArray(data.email) ? data.email[0] : data.email;
|
|
63
|
+
const phone = Array.isArray(data.phone_number) ? data.phone_number[0] : data.phone_number;
|
|
64
|
+
return {
|
|
65
|
+
msclkid: tags.msclkid ? require_click_id_index.formatMsclkid(tags.msclkid) : void 0,
|
|
66
|
+
em: hashEmail(email),
|
|
67
|
+
ph: hashPhone(phone),
|
|
68
|
+
anonymousId: visitor_id || void 0,
|
|
69
|
+
externalId: data.user_id ? sha256(data.user_id) : void 0,
|
|
70
|
+
clientUserAgent: data.user_agent,
|
|
71
|
+
clientIpAddress: typeof tags.ip_address === "string" && tags.ip_address ? tags.ip_address : data.ip_address,
|
|
72
|
+
idfa: platform === "ios" ? tags.advertising_id : void 0,
|
|
73
|
+
gaid: platform === "android" ? tags.advertising_id : void 0
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function getCustomData(event) {
|
|
77
|
+
const [, params] = require_track_uetq.mapUETEvent(event.name, event.properties);
|
|
78
|
+
const customData = {
|
|
79
|
+
eventCategory: params.event_category,
|
|
80
|
+
eventLabel: params.event_label,
|
|
81
|
+
eventValue: params.event_value,
|
|
82
|
+
searchTerm: params.search_term,
|
|
83
|
+
transactionId: params.transaction_id,
|
|
84
|
+
value: params.revenue_value,
|
|
85
|
+
currency: params.currency,
|
|
86
|
+
items: params.items?.map(({ id, name, price, quantity }) => ({
|
|
87
|
+
id,
|
|
88
|
+
name,
|
|
89
|
+
price,
|
|
90
|
+
quantity
|
|
91
|
+
})),
|
|
92
|
+
itemIds: Array.isArray(params.ecomm_prodid) ? params.ecomm_prodid : params.ecomm_prodid ? [params.ecomm_prodid] : void 0,
|
|
93
|
+
pageType: params.ecomm_pagetype,
|
|
94
|
+
ecommTotalValue: params.ecomm_totalvalue,
|
|
95
|
+
ecommCategory: params.ecomm_category
|
|
96
|
+
};
|
|
97
|
+
return Object.values(customData).some((value) => value !== void 0) ? customData : void 0;
|
|
98
|
+
}
|
|
99
|
+
function getServerEvent(event, data = {}, options = {}) {
|
|
100
|
+
const { consent, pageLoads = false } = typeof options === "string" ? { consent: options } : options;
|
|
101
|
+
const { id, name, tags, created_at } = event;
|
|
102
|
+
const isPageLoad = name === "page_view";
|
|
103
|
+
return {
|
|
104
|
+
eventType: isPageLoad ? "pageLoad" : "custom",
|
|
105
|
+
eventId: tags.idempotency_key ?? id,
|
|
106
|
+
eventName: isPageLoad ? void 0 : name,
|
|
107
|
+
eventTime: Math.round(new Date(created_at).getTime() / 1e3),
|
|
108
|
+
eventSourceUrl: require_server_page_location.pageLocation(tags),
|
|
109
|
+
pageLoadId: pageLoads ? tags.page_load_id : void 0,
|
|
110
|
+
referrerUrl: tags.page_referrer,
|
|
111
|
+
pageTitle: tags.page_title,
|
|
112
|
+
adStorageConsent: consent === "granted" ? "G" : consent === "denied" ? "D" : void 0,
|
|
113
|
+
userData: getUserData(event, data),
|
|
114
|
+
customData: isPageLoad ? void 0 : getCustomData(event)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Strip undefined fields so the wire payload carries only what was set. */
|
|
118
|
+
function compact(value) {
|
|
119
|
+
return JSON.parse(JSON.stringify(value));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Send events to `POST /v1/{tagId}/events`. Batches above the API's 1,000-event limit are split
|
|
123
|
+
* into consecutive requests; one response is returned per request made. Never throws: an HTTP
|
|
124
|
+
* or network failure is logged (without the token) and yields no response for that request.
|
|
125
|
+
*/
|
|
126
|
+
async function sendEvents(token, tagId, events, data = {}, options = {}) {
|
|
127
|
+
const { consent, pageLoads = false, dataProvider, continueOnValidationError = true } = options;
|
|
128
|
+
const capiEvents = events.filter((event) => pageLoads && event.name === "page_view" || !require_third_parties_ignored_events.IGNORED_EVENTS.includes(event.name)).map((event) => getServerEvent(event, data, {
|
|
129
|
+
consent,
|
|
130
|
+
pageLoads
|
|
131
|
+
})).filter((event) => event.eventType !== "pageLoad" || event.eventSourceUrl);
|
|
132
|
+
if (capiEvents.length === 0) return [];
|
|
133
|
+
const responses = [];
|
|
134
|
+
for (const batch of (0, _shware_utils.chunk)(capiEvents, MAX_EVENTS_PER_REQUEST)) {
|
|
135
|
+
const body = compact({
|
|
136
|
+
data: batch,
|
|
137
|
+
continueOnValidationError,
|
|
138
|
+
dataProvider
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
const response = await (0, _shware_utils.fetch)(`${ENDPOINT}/${tagId}/events`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: {
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
Accept: "application/json",
|
|
146
|
+
Authorization: `Bearer ${token}`
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify(body)
|
|
149
|
+
});
|
|
150
|
+
if (response.ok) {
|
|
151
|
+
const result = await response.json();
|
|
152
|
+
if (result.error?.details?.length) console.warn(`Microsoft conversion accepted with validation issues: ${JSON.stringify(result.error.details)}`);
|
|
153
|
+
responses.push(result);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const { status } = response;
|
|
157
|
+
const message = await response.text();
|
|
158
|
+
console.error(`Failed to send Microsoft conversion, status: ${status}, body: ${message}`);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
console.error(`Failed to send Microsoft conversion, network error: ${message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return responses;
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
exports.getServerEvent = getServerEvent;
|
|
168
|
+
exports.normalizeEmail = normalizeEmail;
|
|
169
|
+
exports.sendEvents = sendEvents;
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=microsoft-conversions-api.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-conversions-api.cjs","names":["createHash","formatMsclkid","mapUETEvent","pageLocation","IGNORED_EVENTS","chunk","fetch"],"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 /** Links a `custom` event to its `pageLoad` event; a v4 UUID (the SDK's `page_load_id` tag). */\n pageLoadId?: 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, and link every custom event to the\n * page load it happened on (`pageLoadId`, from the `page_load_id` tag). Off by default because\n * the UET JavaScript already reports every page load and the two would double-count\n * destination-URL goals; turn it on for a CAPI-only site with no tag on the page. Off, no\n * `pageLoadId` is sent either: it would point at a page load Microsoft only saw from the tag,\n * under the tag's own id.\n *\n * Not yet modelled: the revenue-only custom event a `pageLoad` needs for a destination goal\n * with variable revenue.\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 type MicrosoftEventOptions = Pick<MicrosoftConversionsOptions, 'consent' | 'pageLoads'>;\n\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData = {},\n /** Options, or — the 8.3.0 signature, still accepted — the bare consent value. */\n options: MicrosoftEventOptions | MicrosoftEventOptions['consent'] = {}\n): MicrosoftEvent {\n const { consent, pageLoads = false } =\n typeof options === 'string' ? { consent: options } : options;\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 pageLoadId: pageLoads ? tags.page_load_id : undefined,\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, pageLoads }))\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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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { TrackEvent, UserProvidedData } from "../track/types.cjs";
|
|
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
|
+
/** Links a `custom` event to its `pageLoad` event; a v4 UUID (the SDK's `page_load_id` tag). */
|
|
56
|
+
pageLoadId?: string;
|
|
57
|
+
referrerUrl?: string;
|
|
58
|
+
pageTitle?: string;
|
|
59
|
+
/** `G` granted, `D` denied. Absent means granted. */
|
|
60
|
+
adStorageConsent?: 'G' | 'D';
|
|
61
|
+
userData: MicrosoftUserData;
|
|
62
|
+
customData?: MicrosoftCustomData;
|
|
63
|
+
}
|
|
64
|
+
interface MicrosoftConversionsOptions {
|
|
65
|
+
/**
|
|
66
|
+
* EEA/UK/CH consent for the whole batch. `denied` events are accepted but used for nothing —
|
|
67
|
+
* no attribution, no audiences. Absent means granted, which is also the API's default.
|
|
68
|
+
*/
|
|
69
|
+
consent?: 'granted' | 'denied';
|
|
70
|
+
/**
|
|
71
|
+
* Also send `page_view` events, as CAPI `pageLoad` events, and link every custom event to the
|
|
72
|
+
* page load it happened on (`pageLoadId`, from the `page_load_id` tag). Off by default because
|
|
73
|
+
* the UET JavaScript already reports every page load and the two would double-count
|
|
74
|
+
* destination-URL goals; turn it on for a CAPI-only site with no tag on the page. Off, no
|
|
75
|
+
* `pageLoadId` is sent either: it would point at a page load Microsoft only saw from the tag,
|
|
76
|
+
* under the tag's own id.
|
|
77
|
+
*
|
|
78
|
+
* Not yet modelled: the revenue-only custom event a `pageLoad` needs for a destination goal
|
|
79
|
+
* with variable revenue.
|
|
80
|
+
*/
|
|
81
|
+
pageLoads?: boolean;
|
|
82
|
+
/** Identifies a third-party sender in Microsoft's monitoring; leave unset for a first-party build. */
|
|
83
|
+
dataProvider?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Process the valid events of a batch and report the invalid ones, instead of the API's default
|
|
86
|
+
* all-or-nothing rejection. On by default — one malformed event should cost itself, not the
|
|
87
|
+
* batch it travelled with.
|
|
88
|
+
*/
|
|
89
|
+
continueOnValidationError?: boolean;
|
|
90
|
+
}
|
|
91
|
+
interface MicrosoftValidationDetail {
|
|
92
|
+
index?: number;
|
|
93
|
+
propertyName?: string;
|
|
94
|
+
attemptedValue?: unknown;
|
|
95
|
+
errorMessage?: string;
|
|
96
|
+
errorCode?: string;
|
|
97
|
+
isWarning?: boolean;
|
|
98
|
+
}
|
|
99
|
+
interface MicrosoftConversionsResponse {
|
|
100
|
+
eventsReceived?: number;
|
|
101
|
+
error?: {
|
|
102
|
+
code?: string;
|
|
103
|
+
message?: string;
|
|
104
|
+
details?: MicrosoftValidationDetail[];
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build one CAPI event. `page_view` becomes a `pageLoad` event (the caller decides whether to
|
|
109
|
+
* send those, see `MicrosoftConversionsOptions.pageLoads`); everything else is a `custom` event
|
|
110
|
+
* named after the track event, which is also the UET action the browser sent.
|
|
111
|
+
*/
|
|
112
|
+
type MicrosoftEventOptions = Pick<MicrosoftConversionsOptions, 'consent' | 'pageLoads'>;
|
|
113
|
+
declare function getServerEvent(event: TrackEvent<any>, data?: UserProvidedData,
|
|
114
|
+
/** Options, or — the 8.3.0 signature, still accepted — the bare consent value. */
|
|
115
|
+
options?: MicrosoftEventOptions | MicrosoftEventOptions['consent']): MicrosoftEvent;
|
|
116
|
+
/**
|
|
117
|
+
* Send events to `POST /v1/{tagId}/events`. Batches above the API's 1,000-event limit are split
|
|
118
|
+
* into consecutive requests; one response is returned per request made. Never throws: an HTTP
|
|
119
|
+
* or network failure is logged (without the token) and yields no response for that request.
|
|
120
|
+
*/
|
|
121
|
+
declare function sendEvents(token: string, tagId: string | number, events: TrackEvent<any>[], data?: UserProvidedData, options?: MicrosoftConversionsOptions): Promise<MicrosoftConversionsResponse[]>;
|
|
122
|
+
//#endregion
|
|
123
|
+
export { MicrosoftConversionsOptions, MicrosoftConversionsResponse, MicrosoftCustomData, MicrosoftEvent, MicrosoftEventOptions, MicrosoftItem, MicrosoftUserData, MicrosoftValidationDetail, getServerEvent, normalizeEmail, sendEvents };
|
|
124
|
+
//# sourceMappingURL=microsoft-conversions-api.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microsoft-conversions-api.d.cts","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;;EAEA;EACA;EACA;;EAEA;EACA,UAAU;EACV,aAAa;;UAGE;;;;;EAKf;;;;;;;;;;;;EAYA;;EAEA;;;;;;EAMA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;IAAU;IAAe;IAAkB,UAAU;;;;;;;;KA2D3C,wBAAwB,KAAK;iBAEzB,eAEd,OAAO,iBACP,OAAM,kBAEN;;AAAA,UAAS,wBAAwB,mCAChC;;;;;;iBA+BmB,WACpB,eACA,wBAEA,QAAQ,mBACR,OAAM,kBACN,UAAS,8BACR,QAAQ"}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
/** Links a `custom` event to its `pageLoad` event; a v4 UUID (the SDK's `page_load_id` tag). */
|
|
56
|
+
pageLoadId?: string;
|
|
57
|
+
referrerUrl?: string;
|
|
58
|
+
pageTitle?: string;
|
|
59
|
+
/** `G` granted, `D` denied. Absent means granted. */
|
|
60
|
+
adStorageConsent?: 'G' | 'D';
|
|
61
|
+
userData: MicrosoftUserData;
|
|
62
|
+
customData?: MicrosoftCustomData;
|
|
63
|
+
}
|
|
64
|
+
interface MicrosoftConversionsOptions {
|
|
65
|
+
/**
|
|
66
|
+
* EEA/UK/CH consent for the whole batch. `denied` events are accepted but used for nothing —
|
|
67
|
+
* no attribution, no audiences. Absent means granted, which is also the API's default.
|
|
68
|
+
*/
|
|
69
|
+
consent?: 'granted' | 'denied';
|
|
70
|
+
/**
|
|
71
|
+
* Also send `page_view` events, as CAPI `pageLoad` events, and link every custom event to the
|
|
72
|
+
* page load it happened on (`pageLoadId`, from the `page_load_id` tag). Off by default because
|
|
73
|
+
* the UET JavaScript already reports every page load and the two would double-count
|
|
74
|
+
* destination-URL goals; turn it on for a CAPI-only site with no tag on the page. Off, no
|
|
75
|
+
* `pageLoadId` is sent either: it would point at a page load Microsoft only saw from the tag,
|
|
76
|
+
* under the tag's own id.
|
|
77
|
+
*
|
|
78
|
+
* Not yet modelled: the revenue-only custom event a `pageLoad` needs for a destination goal
|
|
79
|
+
* with variable revenue.
|
|
80
|
+
*/
|
|
81
|
+
pageLoads?: boolean;
|
|
82
|
+
/** Identifies a third-party sender in Microsoft's monitoring; leave unset for a first-party build. */
|
|
83
|
+
dataProvider?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Process the valid events of a batch and report the invalid ones, instead of the API's default
|
|
86
|
+
* all-or-nothing rejection. On by default — one malformed event should cost itself, not the
|
|
87
|
+
* batch it travelled with.
|
|
88
|
+
*/
|
|
89
|
+
continueOnValidationError?: boolean;
|
|
90
|
+
}
|
|
91
|
+
interface MicrosoftValidationDetail {
|
|
92
|
+
index?: number;
|
|
93
|
+
propertyName?: string;
|
|
94
|
+
attemptedValue?: unknown;
|
|
95
|
+
errorMessage?: string;
|
|
96
|
+
errorCode?: string;
|
|
97
|
+
isWarning?: boolean;
|
|
98
|
+
}
|
|
99
|
+
interface MicrosoftConversionsResponse {
|
|
100
|
+
eventsReceived?: number;
|
|
101
|
+
error?: {
|
|
102
|
+
code?: string;
|
|
103
|
+
message?: string;
|
|
104
|
+
details?: MicrosoftValidationDetail[];
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build one CAPI event. `page_view` becomes a `pageLoad` event (the caller decides whether to
|
|
109
|
+
* send those, see `MicrosoftConversionsOptions.pageLoads`); everything else is a `custom` event
|
|
110
|
+
* named after the track event, which is also the UET action the browser sent.
|
|
111
|
+
*/
|
|
112
|
+
type MicrosoftEventOptions = Pick<MicrosoftConversionsOptions, 'consent' | 'pageLoads'>;
|
|
113
|
+
declare function getServerEvent(event: TrackEvent<any>, data?: UserProvidedData,
|
|
114
|
+
/** Options, or — the 8.3.0 signature, still accepted — the bare consent value. */
|
|
115
|
+
options?: MicrosoftEventOptions | MicrosoftEventOptions['consent']): MicrosoftEvent;
|
|
116
|
+
/**
|
|
117
|
+
* Send events to `POST /v1/{tagId}/events`. Batches above the API's 1,000-event limit are split
|
|
118
|
+
* into consecutive requests; one response is returned per request made. Never throws: an HTTP
|
|
119
|
+
* or network failure is logged (without the token) and yields no response for that request.
|
|
120
|
+
*/
|
|
121
|
+
declare function sendEvents(token: string, tagId: string | number, events: TrackEvent<any>[], data?: UserProvidedData, options?: MicrosoftConversionsOptions): Promise<MicrosoftConversionsResponse[]>;
|
|
122
|
+
//#endregion
|
|
123
|
+
export { MicrosoftConversionsOptions, MicrosoftConversionsResponse, MicrosoftCustomData, MicrosoftEvent, MicrosoftEventOptions, MicrosoftItem, MicrosoftUserData, MicrosoftValidationDetail, getServerEvent, normalizeEmail, sendEvents };
|
|
124
|
+
//# 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;;EAEA;EACA;EACA;;EAEA;EACA,UAAU;EACV,aAAa;;UAGE;;;;;EAKf;;;;;;;;;;;;EAYA;;EAEA;;;;;;EAMA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;IAAU;IAAe;IAAkB,UAAU;;;;;;;;KA2D3C,wBAAwB,KAAK;iBAEzB,eAEd,OAAO,iBACP,OAAM,kBAEN;;AAAA,UAAS,wBAAwB,mCAChC;;;;;;iBA+BmB,WACpB,eACA,wBAEA,QAAQ,mBACR,OAAM,kBACN,UAAS,8BACR,QAAQ"}
|