@shware/analytics 7.5.0 → 7.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/next/index.cjs +1 -1
- package/dist/next/index.mjs +1 -1
- package/dist/react-router/index.cjs +1 -1
- package/dist/react-router/index.mjs +1 -1
- package/dist/schema/index.cjs +7 -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 +7 -0
- package/dist/schema/index.mjs.map +1 -1
- package/dist/server/index.cjs +3 -0
- package/dist/server/index.d.cts +2 -1
- package/dist/server/index.d.mts +2 -1
- package/dist/server/index.mjs +2 -1
- package/dist/server/meta-capi.cjs +258 -0
- package/dist/server/meta-capi.cjs.map +1 -0
- package/dist/server/meta-capi.d.cts +66 -0
- package/dist/server/meta-capi.d.cts.map +1 -0
- package/dist/server/meta-capi.d.mts +66 -0
- package/dist/server/meta-capi.d.mts.map +1 -0
- package/dist/server/meta-capi.mjs +256 -0
- package/dist/server/meta-capi.mjs.map +1 -0
- package/dist/server/meta-conversions-api.cjs +4 -2
- package/dist/server/meta-conversions-api.cjs.map +1 -1
- package/dist/server/meta-conversions-api.d.cts.map +1 -1
- package/dist/server/meta-conversions-api.d.mts.map +1 -1
- package/dist/server/meta-conversions-api.mjs +4 -2
- package/dist/server/meta-conversions-api.mjs.map +1 -1
- package/dist/server/openai-conversions-api.cjs +2 -1
- package/dist/server/openai-conversions-api.cjs.map +1 -1
- package/dist/server/openai-conversions-api.d.cts.map +1 -1
- package/dist/server/openai-conversions-api.d.mts.map +1 -1
- package/dist/server/openai-conversions-api.mjs +2 -1
- package/dist/server/openai-conversions-api.mjs.map +1 -1
- package/dist/server/page-location.cjs +22 -0
- package/dist/server/page-location.cjs.map +1 -0
- package/dist/server/page-location.d.cts +19 -0
- package/dist/server/page-location.d.cts.map +1 -0
- package/dist/server/page-location.d.mts +19 -0
- package/dist/server/page-location.d.mts.map +1 -0
- package/dist/server/page-location.mjs +21 -0
- package/dist/server/page-location.mjs.map +1 -0
- package/dist/track/types.d.cts +6 -0
- package/dist/track/types.d.cts.map +1 -1
- package/dist/track/types.d.mts +6 -0
- package/dist/track/types.d.mts.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,258 @@
|
|
|
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_action_source = require("./action-source.cjs");
|
|
5
|
+
const require_track_fbq = require("../track/fbq.cjs");
|
|
6
|
+
const require_server_page_location = require("./page-location.cjs");
|
|
7
|
+
let _shware_utils = require("@shware/utils");
|
|
8
|
+
let node_crypto = require("node:crypto");
|
|
9
|
+
let capi_param_builder_nodejs = require("capi-param-builder-nodejs");
|
|
10
|
+
//#region src/server/meta-capi.ts
|
|
11
|
+
/**
|
|
12
|
+
* Meta Conversions API sender built on `capi-param-builder-nodejs` and plain `fetch`, replacing
|
|
13
|
+
* the 31MB `facebook-nodejs-business-sdk` with ~0.5MB of parameter building. `sendMetaEvents`
|
|
14
|
+
* stays exported and untouched; this module is the drop-in successor hosts opt into.
|
|
15
|
+
*
|
|
16
|
+
* The wire payload is byte-identical to what the business SDK's `ServerEvent.normalize()`
|
|
17
|
+
* produces — enforced by a differential test that runs both builders over the same events.
|
|
18
|
+
* Where the two libraries normalize differently (the business SDK keeps punctuation in names,
|
|
19
|
+
* spaces in multi-word last names, accents in cities; the param builder strips them per the
|
|
20
|
+
* current documented rules), this module follows the business SDK so that switching senders
|
|
21
|
+
* cannot change a single hash Meta receives. The param builder is used where the two provably
|
|
22
|
+
* agree — email, phone, gender, country — and its adoption-tracking appendix (`<hash>.<token>`)
|
|
23
|
+
* is stripped for the same reason.
|
|
24
|
+
*
|
|
25
|
+
* Two deliberate behavior differences from the business SDK, both on invalid input only:
|
|
26
|
+
* an invalid field (malformed email, non-ISO country, letters in a phone) is dropped from
|
|
27
|
+
* user_data instead of throwing inside `execute()` and costing the whole batch; an invalid
|
|
28
|
+
* currency is uppercased and forwarded instead of rejected. Valid input is unaffected.
|
|
29
|
+
*
|
|
30
|
+
* https://developers.facebook.com/docs/marketing-api/conversions-api/using-the-api
|
|
31
|
+
* https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
|
|
32
|
+
*/
|
|
33
|
+
/** Matches the Graph API version the pinned business SDK speaks (`FacebookAdsApi.VERSION`). */
|
|
34
|
+
const API_VERSION = "v24.0";
|
|
35
|
+
const USER_ASSIGNED_COUNTRIES = ["xk"];
|
|
36
|
+
function normalizeCountry(input) {
|
|
37
|
+
const country = input?.split(/[-_]/).at(0)?.toLowerCase();
|
|
38
|
+
if (!country) return void 0;
|
|
39
|
+
return USER_ASSIGNED_COUNTRIES.includes(country) ? void 0 : country;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Lazy for the same reason as the session and the token bucket (see setup/session.ts):
|
|
43
|
+
* Cloudflare Workers refuse some work in module scope, and nothing here is per-request state.
|
|
44
|
+
*/
|
|
45
|
+
let paramBuilder;
|
|
46
|
+
/**
|
|
47
|
+
* Normalize and hash one PII value through the param builder, stripped to the bare SHA-256 the
|
|
48
|
+
* business SDK sends: the builder appends a `.<language-token>` adoption marker that Meta's
|
|
49
|
+
* endpoint tolerates but the business SDK never produced. Pre-hashed input passes through, as
|
|
50
|
+
* it does in the business SDK.
|
|
51
|
+
*/
|
|
52
|
+
function pii(value, dataType) {
|
|
53
|
+
if (!value) return void 0;
|
|
54
|
+
return (paramBuilder ??= new capi_param_builder_nodejs.ParamBuilder()).getNormalizedAndHashedPII(value, dataType)?.split(".").at(0) ?? void 0;
|
|
55
|
+
}
|
|
56
|
+
const SHA256_OR_MD5 = /^[a-f0-9]{64}$|^[a-f0-9]{32}$/;
|
|
57
|
+
/**
|
|
58
|
+
* The business SDK's `normalizeAndHash` for the fields where the param builder normalizes
|
|
59
|
+
* differently (names, city, state, zip): trim + lowercase, pass a pre-hashed value through,
|
|
60
|
+
* apply the field rule, SHA-256. Field rules are copied from `ServerSideUtils` verbatim so the
|
|
61
|
+
* differential test can hold the outputs equal.
|
|
62
|
+
*/
|
|
63
|
+
function hashed(input, rule) {
|
|
64
|
+
if (!input) return void 0;
|
|
65
|
+
const lowered = input.trim().toLowerCase();
|
|
66
|
+
if (!lowered) return void 0;
|
|
67
|
+
if (SHA256_OR_MD5.test(lowered)) return lowered;
|
|
68
|
+
const normalized = rule(lowered);
|
|
69
|
+
if (normalized === void 0) return void 0;
|
|
70
|
+
return (0, node_crypto.createHash)("sha256").update(normalized).digest("hex");
|
|
71
|
+
}
|
|
72
|
+
const nameRule = (v) => v;
|
|
73
|
+
const f5Rule = (v) => v.length <= 5 ? v : v.slice(0, 5);
|
|
74
|
+
const cityStateRule = (v) => v.replace(/[0-9\s().-]/g, "");
|
|
75
|
+
const zipRule = (v) => {
|
|
76
|
+
const zip = v.replace(/\s/g, "").split("-", 1)[0];
|
|
77
|
+
return zip.length < 2 ? void 0 : zip;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Every multi-value field is deduplicated after hashing, order preserved — the SDK runs each
|
|
81
|
+
* through `dedupArray` (a Set), `external_id` included.
|
|
82
|
+
*/
|
|
83
|
+
const dedupe = (hashes) => Array.from(new Set(hashes.filter((hash) => hash !== void 0)));
|
|
84
|
+
const list = (hashes) => {
|
|
85
|
+
const deduped = dedupe(hashes);
|
|
86
|
+
return deduped.length > 0 ? deduped : void 0;
|
|
87
|
+
};
|
|
88
|
+
function getUserData(tags, data, eventTimeMs) {
|
|
89
|
+
const userData = {};
|
|
90
|
+
if (data.email) {
|
|
91
|
+
const emails = Array.isArray(data.email) ? data.email : [data.email];
|
|
92
|
+
userData.em = list(emails.map((email) => pii(email, capi_param_builder_nodejs.PII_DATA_TYPE.EMAIL)));
|
|
93
|
+
}
|
|
94
|
+
if (data.phone_number) {
|
|
95
|
+
const phones = Array.isArray(data.phone_number) ? data.phone_number : [data.phone_number];
|
|
96
|
+
userData.ph = list(phones.map((phone) => pii(phone, capi_param_builder_nodejs.PII_DATA_TYPE.PHONE)));
|
|
97
|
+
}
|
|
98
|
+
if (data.gender === "female" || data.gender === "male") userData.ge = list([pii(data.gender === "female" ? "f" : "m", capi_param_builder_nodejs.PII_DATA_TYPE.GENDER)]);
|
|
99
|
+
if (data.address) if (Array.isArray(data.address)) {
|
|
100
|
+
userData.fn = dedupe(data.address.map((a) => hashed(a.first_name, nameRule)));
|
|
101
|
+
userData.ln = dedupe(data.address.map((a) => hashed(a.last_name, nameRule)));
|
|
102
|
+
userData.ct = dedupe(data.address.map((a) => hashed(a.city, cityStateRule)));
|
|
103
|
+
userData.st = dedupe(data.address.map((a) => hashed(a.region, cityStateRule)));
|
|
104
|
+
userData.zp = dedupe(data.address.map((a) => hashed(a.postal_code, zipRule)));
|
|
105
|
+
userData.country = dedupe(data.address.map((a) => pii(normalizeCountry(a.country), capi_param_builder_nodejs.PII_DATA_TYPE.COUNTRY)));
|
|
106
|
+
} else {
|
|
107
|
+
userData.fn = list([hashed(data.address.first_name, nameRule)]);
|
|
108
|
+
userData.ln = list([hashed(data.address.last_name, nameRule)]);
|
|
109
|
+
userData.ct = list([hashed(data.address.city, cityStateRule)]);
|
|
110
|
+
userData.st = list([hashed(data.address.region, cityStateRule)]);
|
|
111
|
+
userData.zp = list([hashed(data.address.postal_code, zipRule)]);
|
|
112
|
+
userData.country = list([pii(normalizeCountry(data.address.country), capi_param_builder_nodejs.PII_DATA_TYPE.COUNTRY)]);
|
|
113
|
+
userData.f5first = hashed(data.address.first_name?.slice(0, 5), f5Rule);
|
|
114
|
+
userData.f5last = hashed(data.address.last_name?.slice(0, 5), f5Rule);
|
|
115
|
+
}
|
|
116
|
+
if (data.birthday) {
|
|
117
|
+
const { year, month, day } = data.birthday;
|
|
118
|
+
if (/^\d{4}$/.test(year.toString())) userData.doby = (0, node_crypto.createHash)("sha256").update(year.toString()).digest("hex");
|
|
119
|
+
if (month >= 1 && month <= 12) userData.dobm = (0, node_crypto.createHash)("sha256").update(month.toString().padStart(2, "0")).digest("hex");
|
|
120
|
+
if (day >= 1 && day <= 31) userData.dobd = (0, node_crypto.createHash)("sha256").update(day.toString().padStart(2, "0")).digest("hex");
|
|
121
|
+
}
|
|
122
|
+
if (data.user_id && data.user_id.length !== 0) userData.external_id = [data.user_id];
|
|
123
|
+
if (data.ip_address) userData.client_ip_address = data.ip_address;
|
|
124
|
+
if (data.user_agent) userData.client_user_agent = data.user_agent;
|
|
125
|
+
if (data.fb_login_id) userData.fb_login_id = data.fb_login_id;
|
|
126
|
+
if (data.fb_page_id) userData.page_id = data.fb_page_id;
|
|
127
|
+
if (tags.fbc) userData.fbc = tags.fbc;
|
|
128
|
+
else if (tags.fbclid) userData.fbc = require_click_id_index.formatFbc(tags.fbclid, eventTimeMs);
|
|
129
|
+
if (tags.fbp) userData.fbp = tags.fbp;
|
|
130
|
+
if (tags.advertising_id) userData.madid = tags.advertising_id;
|
|
131
|
+
if (tags.ip_address && typeof tags.ip_address === "string") userData.client_ip_address = tags.ip_address;
|
|
132
|
+
return userData;
|
|
133
|
+
}
|
|
134
|
+
const DELIVERY_CATEGORIES = [
|
|
135
|
+
"in_store",
|
|
136
|
+
"curbside",
|
|
137
|
+
"home_delivery"
|
|
138
|
+
];
|
|
139
|
+
function getCustomData({ name, properties }) {
|
|
140
|
+
const [_, _name, fbEventProperties] = require_track_fbq.mapFBEvent(name, properties);
|
|
141
|
+
const { value, currency, content_name, content_category, content_ids, contents, content_type, predicted_ltv, num_items, search_string, status, delivery_category, ...custom_properties } = fbEventProperties;
|
|
142
|
+
const customData = {};
|
|
143
|
+
if (value !== void 0 && value !== null && Number.isFinite(Number.parseFloat(String(value)))) customData.value = Number.parseFloat(String(value));
|
|
144
|
+
if (currency) customData.currency = currency.trim().toUpperCase().replace(/[^A-Z]/g, "");
|
|
145
|
+
if (content_name) customData.content_name = content_name;
|
|
146
|
+
if (content_category) customData.content_category = content_category;
|
|
147
|
+
if (content_ids) customData.content_ids = content_ids;
|
|
148
|
+
if (contents) customData.contents = contents.map((c) => {
|
|
149
|
+
const content = {};
|
|
150
|
+
if (c.id) content.id = c.id;
|
|
151
|
+
if (c.quantity) content.quantity = c.quantity;
|
|
152
|
+
if (c.item_price) content.item_price = c.item_price;
|
|
153
|
+
if (c.title) content.title = c.title;
|
|
154
|
+
if (c.description) content.description = c.description;
|
|
155
|
+
if (c.brand) content.brand = c.brand;
|
|
156
|
+
if (c.category) content.category = c.category;
|
|
157
|
+
const delivery = c.delivery_category?.trim().toLowerCase();
|
|
158
|
+
if (delivery && DELIVERY_CATEGORIES.includes(delivery)) content.delivery_category = delivery;
|
|
159
|
+
return content;
|
|
160
|
+
});
|
|
161
|
+
if (content_type) customData.content_type = content_type;
|
|
162
|
+
if (predicted_ltv) customData.predicted_ltv = predicted_ltv;
|
|
163
|
+
if (num_items) customData.num_items = num_items;
|
|
164
|
+
if (search_string) customData.search_string = search_string;
|
|
165
|
+
if (status) customData.status = status.toString();
|
|
166
|
+
const delivery = delivery_category?.trim().toLowerCase();
|
|
167
|
+
if (delivery && DELIVERY_CATEGORIES.includes(delivery)) customData.delivery_category = delivery;
|
|
168
|
+
return {
|
|
169
|
+
...customData,
|
|
170
|
+
...custom_properties
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* `extinfo` is keyed by field index. The business SDK builds the same object and leaves unset
|
|
175
|
+
* indexes out when it serializes; the documented 16-element array is what the Graph endpoint
|
|
176
|
+
* decodes this into. Indexes: 0 version, 1 package, 2 short version, 3 long version, 4 OS
|
|
177
|
+
* version, 5 device model, 6 locale, 9 screen width, 10 height, 11 density.
|
|
178
|
+
*/
|
|
179
|
+
function getAppData(tags, appPackageName) {
|
|
180
|
+
const extinfo = {};
|
|
181
|
+
if (tags.os_name === "iOS" || tags.os_name === "iPadOS") extinfo[0] = "i2";
|
|
182
|
+
else if (tags.os_name === "Android") extinfo[0] = "a2";
|
|
183
|
+
extinfo[1] = appPackageName;
|
|
184
|
+
const shortVersion = tags.release?.split(".").at(0);
|
|
185
|
+
if (shortVersion) extinfo[2] = shortVersion;
|
|
186
|
+
if (tags.release) extinfo[3] = tags.release;
|
|
187
|
+
if (tags.os_version) extinfo[4] = tags.os_version;
|
|
188
|
+
if (tags.device_model_id) extinfo[5] = tags.device_model_id;
|
|
189
|
+
if (tags.language) extinfo[6] = tags.language;
|
|
190
|
+
if (tags.screen_width) extinfo[9] = tags.screen_width;
|
|
191
|
+
if (tags.screen_height) extinfo[10] = tags.screen_height;
|
|
192
|
+
if (tags.device_pixel_ratio) extinfo[11] = tags.device_pixel_ratio.toString();
|
|
193
|
+
const appData = { extinfo };
|
|
194
|
+
if (tags.advertising_id) appData.advertiser_tracking_enabled = true;
|
|
195
|
+
if (tags.install_referrer) appData.install_referrer = tags.install_referrer;
|
|
196
|
+
return appData;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* `action_source` is derived from `event.platform` alone: an OS platform is an app, `web` is a
|
|
200
|
+
* website, and everything else — including a backend-built offline conversion, which should
|
|
201
|
+
* declare `platform: 'unknown'` — lands in Meta's `other`, exactly where the business-SDK
|
|
202
|
+
* sender put an explicit `'offline'` too.
|
|
203
|
+
*/
|
|
204
|
+
function getCapiEvent(event, data, appPackageName) {
|
|
205
|
+
const eventTimeMs = new Date(event.created_at).getTime();
|
|
206
|
+
const [_, eventName] = require_track_fbq.mapFBEvent(event.name, event.properties);
|
|
207
|
+
const source = require_server_action_source.resolveActionSource(event.platform);
|
|
208
|
+
const capiEvent = {
|
|
209
|
+
event_name: eventName,
|
|
210
|
+
event_time: Math.round(eventTimeMs / 1e3),
|
|
211
|
+
event_id: event.tags.idempotency_key ?? event.id,
|
|
212
|
+
action_source: source === "app" ? "app" : source === "web" ? "website" : "other",
|
|
213
|
+
user_data: getUserData(event.tags, data, eventTimeMs),
|
|
214
|
+
custom_data: getCustomData(event)
|
|
215
|
+
};
|
|
216
|
+
if (source === "app" && appPackageName) capiEvent.app_data = getAppData(event.tags, appPackageName);
|
|
217
|
+
const eventSourceUrl = require_server_page_location.pageLocation(event.tags);
|
|
218
|
+
if (eventSourceUrl) capiEvent.event_source_url = eventSourceUrl;
|
|
219
|
+
return capiEvent;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The lightweight counterpart to `sendMetaEvents`: same filtering, same payload, same
|
|
223
|
+
* never-throws contract, minus the 31MB SDK. The token travels in the JSON body, never in the
|
|
224
|
+
* URL, and is never logged.
|
|
225
|
+
*/
|
|
226
|
+
async function sendMetaConversions(accessToken, pixelId, events, data = {}, options = {}) {
|
|
227
|
+
const capiEvents = events.filter((event) => !require_third_parties_ignored_events.IGNORED_EVENTS.includes(event.name)).map((event) => getCapiEvent(event, data, options.appPackageName));
|
|
228
|
+
if (capiEvents.length === 0) return void 0;
|
|
229
|
+
const version = options.apiVersion ?? API_VERSION;
|
|
230
|
+
try {
|
|
231
|
+
const response = await (0, _shware_utils.fetch)(`https://graph.facebook.com/${version}/${pixelId}/events`, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: {
|
|
234
|
+
"Content-Type": "application/json",
|
|
235
|
+
Accept: "application/json"
|
|
236
|
+
},
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
data: capiEvents,
|
|
239
|
+
test_event_code: options.testEventCode,
|
|
240
|
+
access_token: accessToken
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
if (response.ok) return await response.json();
|
|
244
|
+
const { status } = response;
|
|
245
|
+
const message = await response.text();
|
|
246
|
+
console.error(`Failed to send Meta conversion, status: ${status}, body: ${message}`);
|
|
247
|
+
return;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
+
console.error(`Failed to send Meta conversion, network error: ${message}`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
exports.getCapiEvent = getCapiEvent;
|
|
256
|
+
exports.sendMetaConversions = sendMetaConversions;
|
|
257
|
+
|
|
258
|
+
//# sourceMappingURL=meta-capi.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta-capi.cjs","names":["ParamBuilder","PII_DATA_TYPE","formatFbc","mapFBEvent","resolveActionSource","pageLocation","IGNORED_EVENTS"],"sources":["../../src/server/meta-capi.ts"],"sourcesContent":["/**\n * Meta Conversions API sender built on `capi-param-builder-nodejs` and plain `fetch`, replacing\n * the 31MB `facebook-nodejs-business-sdk` with ~0.5MB of parameter building. `sendMetaEvents`\n * stays exported and untouched; this module is the drop-in successor hosts opt into.\n *\n * The wire payload is byte-identical to what the business SDK's `ServerEvent.normalize()`\n * produces — enforced by a differential test that runs both builders over the same events.\n * Where the two libraries normalize differently (the business SDK keeps punctuation in names,\n * spaces in multi-word last names, accents in cities; the param builder strips them per the\n * current documented rules), this module follows the business SDK so that switching senders\n * cannot change a single hash Meta receives. The param builder is used where the two provably\n * agree — email, phone, gender, country — and its adoption-tracking appendix (`<hash>.<token>`)\n * is stripped for the same reason.\n *\n * Two deliberate behavior differences from the business SDK, both on invalid input only:\n * an invalid field (malformed email, non-ISO country, letters in a phone) is dropped from\n * user_data instead of throwing inside `execute()` and costing the whole batch; an invalid\n * currency is uppercased and forwarded instead of rejected. Valid input is unaffected.\n *\n * https://developers.facebook.com/docs/marketing-api/conversions-api/using-the-api\n * https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters\n */\nimport { createHash } from 'node:crypto';\nimport { fetch } from '@shware/utils';\nimport { PII_DATA_TYPE, ParamBuilder } from 'capi-param-builder-nodejs';\nimport { formatFbc } from '../click-id/index';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { mapFBEvent } from '../track/fbq';\nimport type { TrackEvent, TrackTags, UserProvidedData } from '../track/types';\nimport { resolveActionSource } from './action-source';\nimport { pageLocation } from './page-location';\n\n/** Matches the Graph API version the pinned business SDK speaks (`FacebookAdsApi.VERSION`). */\nconst API_VERSION = 'v24.0';\n\nconst USER_ASSIGNED_COUNTRIES: string[] = ['xk'];\nfunction normalizeCountry(input: string | undefined): string | undefined {\n const country = input?.split(/[-_]/).at(0)?.toLowerCase();\n if (!country) return undefined;\n return USER_ASSIGNED_COUNTRIES.includes(country) ? undefined : country;\n}\n\n/**\n * Lazy for the same reason as the session and the token bucket (see setup/session.ts):\n * Cloudflare Workers refuse some work in module scope, and nothing here is per-request state.\n */\nlet paramBuilder: ParamBuilder | undefined;\n\n/**\n * Normalize and hash one PII value through the param builder, stripped to the bare SHA-256 the\n * business SDK sends: the builder appends a `.<language-token>` adoption marker that Meta's\n * endpoint tolerates but the business SDK never produced. Pre-hashed input passes through, as\n * it does in the business SDK.\n */\nfunction pii(value: string | undefined, dataType: string): string | undefined {\n if (!value) return undefined;\n const built = (paramBuilder ??= new ParamBuilder()).getNormalizedAndHashedPII(value, dataType);\n return built?.split('.').at(0) ?? undefined;\n}\n\nconst SHA256_OR_MD5 = /^[a-f0-9]{64}$|^[a-f0-9]{32}$/;\n\n/**\n * The business SDK's `normalizeAndHash` for the fields where the param builder normalizes\n * differently (names, city, state, zip): trim + lowercase, pass a pre-hashed value through,\n * apply the field rule, SHA-256. Field rules are copied from `ServerSideUtils` verbatim so the\n * differential test can hold the outputs equal.\n */\nfunction hashed(\n input: string | undefined,\n rule: (lowered: string) => string | undefined\n): string | undefined {\n if (!input) return undefined;\n const lowered = input.trim().toLowerCase();\n if (!lowered) return undefined;\n if (SHA256_OR_MD5.test(lowered)) return lowered;\n const normalized = rule(lowered);\n if (normalized === undefined) return undefined;\n return createHash('sha256').update(normalized).digest('hex');\n}\n\nconst nameRule = (v: string) => v;\nconst f5Rule = (v: string) => (v.length <= 5 ? v : v.slice(0, 5));\nconst cityStateRule = (v: string) => v.replace(/[0-9\\s().-]/g, '');\nconst zipRule = (v: string) => {\n const zip = v.replace(/\\s/g, '').split('-', 1)[0];\n return zip.length < 2 ? undefined : zip;\n};\n\n/**\n * Every multi-value field is deduplicated after hashing, order preserved — the SDK runs each\n * through `dedupArray` (a Set), `external_id` included.\n */\nconst dedupe = (hashes: (string | undefined)[]) =>\n Array.from(new Set(hashes.filter((hash): hash is string => hash !== undefined)));\n\nconst list = (hashes: (string | undefined)[]) => {\n const deduped = dedupe(hashes);\n return deduped.length > 0 ? deduped : undefined;\n};\n\ninterface CapiUserData {\n em?: string[];\n ph?: string[];\n ge?: string[];\n fn?: string[];\n ln?: string[];\n ct?: string[];\n st?: string[];\n zp?: string[];\n country?: string[];\n external_id?: string[];\n client_ip_address?: string;\n client_user_agent?: string;\n fbc?: string;\n fbp?: string;\n fb_login_id?: string;\n page_id?: string;\n f5first?: string;\n f5last?: string;\n dobd?: string;\n dobm?: string;\n doby?: string;\n madid?: string;\n}\n\nfunction getUserData(tags: TrackTags, data: UserProvidedData, eventTimeMs: number): CapiUserData {\n const userData: CapiUserData = {};\n\n if (data.email) {\n const emails = Array.isArray(data.email) ? data.email : [data.email];\n userData.em = list(emails.map((email) => pii(email, PII_DATA_TYPE.EMAIL)));\n }\n if (data.phone_number) {\n const phones = Array.isArray(data.phone_number) ? data.phone_number : [data.phone_number];\n userData.ph = list(phones.map((phone) => pii(phone, PII_DATA_TYPE.PHONE)));\n }\n if (data.gender === 'female' || data.gender === 'male') {\n userData.ge = list([pii(data.gender === 'female' ? 'f' : 'm', PII_DATA_TYPE.GENDER)]);\n }\n if (data.address) {\n if (Array.isArray(data.address)) {\n // The SDK sets these lists unconditionally in the multi-address branch, so an all-empty\n // column still goes out as `[]` — kept for byte-equality with the current sender.\n userData.fn = dedupe(data.address.map((a) => hashed(a.first_name, nameRule)));\n userData.ln = dedupe(data.address.map((a) => hashed(a.last_name, nameRule)));\n userData.ct = dedupe(data.address.map((a) => hashed(a.city, cityStateRule)));\n userData.st = dedupe(data.address.map((a) => hashed(a.region, cityStateRule)));\n userData.zp = dedupe(data.address.map((a) => hashed(a.postal_code, zipRule)));\n userData.country = dedupe(\n data.address.map((a) => pii(normalizeCountry(a.country), PII_DATA_TYPE.COUNTRY))\n );\n } else {\n userData.fn = list([hashed(data.address.first_name, nameRule)]);\n userData.ln = list([hashed(data.address.last_name, nameRule)]);\n userData.ct = list([hashed(data.address.city, cityStateRule)]);\n userData.st = list([hashed(data.address.region, cityStateRule)]);\n userData.zp = list([hashed(data.address.postal_code, zipRule)]);\n userData.country = list([pii(normalizeCountry(data.address.country), PII_DATA_TYPE.COUNTRY)]);\n // The 5-character prefixes, exactly as the current sender builds them: sliced first, then\n // normalized. Only the single-address branch sets them, as before.\n userData.f5first = hashed(data.address.first_name?.slice(0, 5), f5Rule);\n userData.f5last = hashed(data.address.last_name?.slice(0, 5), f5Rule);\n }\n }\n if (data.birthday) {\n const { year, month, day } = data.birthday;\n if (/^\\d{4}$/.test(year.toString())) {\n userData.doby = createHash('sha256').update(year.toString()).digest('hex');\n }\n if (month >= 1 && month <= 12) {\n userData.dobm = createHash('sha256').update(month.toString().padStart(2, '0')).digest('hex');\n }\n if (day >= 1 && day <= 31) {\n userData.dobd = createHash('sha256').update(day.toString().padStart(2, '0')).digest('hex');\n }\n }\n if (data.user_id && data.user_id.length !== 0) {\n // Unhashed, as the business SDK sends it. Meta documents hashing as recommended, not\n // required; changing it here would break continuity with every event sent so far.\n userData.external_id = [data.user_id];\n }\n if (data.ip_address) {\n userData.client_ip_address = data.ip_address;\n }\n if (data.user_agent) {\n userData.client_user_agent = data.user_agent;\n }\n if (data.fb_login_id) {\n userData.fb_login_id = data.fb_login_id;\n }\n if (data.fb_page_id) {\n userData.page_id = data.fb_page_id;\n }\n\n if (tags.fbc) {\n userData.fbc = tags.fbc;\n } else if (tags.fbclid) {\n userData.fbc = formatFbc(tags.fbclid, eventTimeMs);\n }\n if (tags.fbp) {\n userData.fbp = tags.fbp;\n }\n if (tags.advertising_id) {\n userData.madid = tags.advertising_id;\n }\n if (tags.ip_address && typeof tags.ip_address === 'string') {\n userData.client_ip_address = tags.ip_address;\n }\n\n return userData;\n}\n\nconst DELIVERY_CATEGORIES = ['in_store', 'curbside', 'home_delivery'];\n\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getCustomData({ name, properties }: TrackEvent<any>): Record<string, unknown> {\n const [_, _name, fbEventProperties] = mapFBEvent(name, properties);\n const {\n value,\n currency,\n content_name,\n content_category,\n content_ids,\n contents,\n content_type,\n predicted_ltv,\n num_items,\n search_string,\n status,\n delivery_category,\n ...custom_properties\n } = fbEventProperties;\n\n const customData: Record<string, unknown> = {};\n // oxlint-disable-next-line typescript/no-unnecessary-condition -- custom event properties may carry `value: null` at runtime despite the declared type\n if (value !== undefined && value !== null && Number.isFinite(Number.parseFloat(String(value)))) {\n customData.value = Number.parseFloat(String(value));\n }\n if (currency) {\n // The business SDK also validates against the ISO 4217 list and throws; here an unknown\n // code is forwarded uppercased rather than costing the batch.\n customData.currency = currency.trim().toUpperCase().replace(/[^A-Z]/g, '');\n }\n if (content_name) customData.content_name = content_name;\n if (content_category) customData.content_category = content_category;\n if (content_ids) customData.content_ids = content_ids;\n if (contents) {\n customData.contents = contents.map((c) => {\n const content: Record<string, unknown> = {};\n if (c.id) content.id = c.id;\n if (c.quantity) content.quantity = c.quantity;\n if (c.item_price) content.item_price = c.item_price;\n if (c.title) content.title = c.title;\n if (c.description) content.description = c.description;\n if (c.brand) content.brand = c.brand;\n if (c.category) content.category = c.category;\n const delivery = c.delivery_category?.trim().toLowerCase();\n if (delivery && DELIVERY_CATEGORIES.includes(delivery)) content.delivery_category = delivery;\n return content;\n });\n }\n if (content_type) customData.content_type = content_type;\n if (predicted_ltv) customData.predicted_ltv = predicted_ltv;\n if (num_items) customData.num_items = num_items;\n if (search_string) customData.search_string = search_string;\n if (status) customData.status = status.toString();\n const delivery = delivery_category?.trim().toLowerCase();\n if (delivery && DELIVERY_CATEGORIES.includes(delivery)) customData.delivery_category = delivery;\n\n return { ...customData, ...custom_properties };\n}\n\n/**\n * `extinfo` is keyed by field index. The business SDK builds the same object and leaves unset\n * indexes out when it serializes; the documented 16-element array is what the Graph endpoint\n * decodes this into. Indexes: 0 version, 1 package, 2 short version, 3 long version, 4 OS\n * version, 5 device model, 6 locale, 9 screen width, 10 height, 11 density.\n */\nfunction getAppData(tags: TrackTags, appPackageName: string): Record<string, unknown> {\n const extinfo: Record<number, string | number> = {};\n if (tags.os_name === 'iOS' || tags.os_name === 'iPadOS') {\n extinfo[0] = 'i2';\n } else if (tags.os_name === 'Android') {\n extinfo[0] = 'a2';\n }\n extinfo[1] = appPackageName;\n const shortVersion = tags.release?.split('.').at(0);\n if (shortVersion) extinfo[2] = shortVersion;\n if (tags.release) extinfo[3] = tags.release;\n if (tags.os_version) extinfo[4] = tags.os_version;\n if (tags.device_model_id) extinfo[5] = tags.device_model_id;\n if (tags.language) extinfo[6] = tags.language;\n if (tags.screen_width) extinfo[9] = tags.screen_width;\n if (tags.screen_height) extinfo[10] = tags.screen_height;\n if (tags.device_pixel_ratio) extinfo[11] = tags.device_pixel_ratio.toString();\n\n const appData: Record<string, unknown> = { extinfo };\n if (tags.advertising_id) appData.advertiser_tracking_enabled = true;\n if (tags.install_referrer) appData.install_referrer = tags.install_referrer;\n return appData;\n}\n\n/** One entry of the `data` array POSTed to `/{pixel_id}/events`. */\nexport interface CapiEvent {\n event_name: string;\n event_time: number;\n event_id: string;\n event_source_url?: string;\n action_source: 'app' | 'website' | 'other';\n user_data: CapiUserData;\n custom_data: Record<string, unknown>;\n app_data?: Record<string, unknown>;\n}\n\n/**\n * `action_source` is derived from `event.platform` alone: an OS platform is an app, `web` is a\n * website, and everything else — including a backend-built offline conversion, which should\n * declare `platform: 'unknown'` — lands in Meta's `other`, exactly where the business-SDK\n * sender put an explicit `'offline'` too.\n */\nexport function getCapiEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData,\n appPackageName?: string\n): CapiEvent {\n const eventTimeMs = new Date(event.created_at).getTime();\n const [_, eventName] = mapFBEvent(event.name, event.properties);\n const source = resolveActionSource(event.platform);\n\n const capiEvent: CapiEvent = {\n event_name: eventName,\n event_time: Math.round(eventTimeMs / 1000),\n event_id: event.tags.idempotency_key ?? event.id,\n // `action_source` is required; an offline conversion and an undeterminable platform both\n // land in Meta's catch-all, exactly as in the business-SDK sender.\n action_source: source === 'app' ? 'app' : source === 'web' ? 'website' : 'other',\n user_data: getUserData(event.tags, data, eventTimeMs),\n custom_data: getCustomData(event),\n };\n if (source === 'app' && appPackageName) {\n capiEvent.app_data = getAppData(event.tags, appPackageName);\n }\n const eventSourceUrl = pageLocation(event.tags);\n if (eventSourceUrl) {\n capiEvent.event_source_url = eventSourceUrl;\n }\n return capiEvent;\n}\n\nexport interface MetaConversionsOptions {\n /** Attaches `app_data`/extinfo to events whose platform resolves to an app. */\n appPackageName?: string;\n /** Routes the batch to Events Manager's Test Events tab instead of production. */\n testEventCode?: string;\n /** Graph API version, e.g. 'v24.0'. */\n apiVersion?: string;\n}\n\nexport interface MetaConversionsResponse {\n events_received?: number;\n messages?: string[];\n fbtrace_id?: string;\n}\n\n/**\n * The lightweight counterpart to `sendMetaEvents`: same filtering, same payload, same\n * never-throws contract, minus the 31MB SDK. The token travels in the JSON body, never in the\n * URL, and is never logged.\n */\nexport async function sendMetaConversions(\n accessToken: string,\n pixelId: string,\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n events: TrackEvent<any>[],\n data: UserProvidedData = {},\n options: MetaConversionsOptions = {}\n): Promise<MetaConversionsResponse | undefined> {\n const capiEvents = events\n .filter((event) => !IGNORED_EVENTS.includes(event.name))\n .map((event) => getCapiEvent(event, data, options.appPackageName));\n if (capiEvents.length === 0) return undefined;\n\n const version = options.apiVersion ?? API_VERSION;\n try {\n const response = await fetch(`https://graph.facebook.com/${version}/${pixelId}/events`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify({\n data: capiEvents,\n test_event_code: options.testEventCode,\n access_token: accessToken,\n }),\n });\n if (response.ok) return (await response.json()) as MetaConversionsResponse;\n const { status } = response;\n const message = await response.text();\n console.error(`Failed to send Meta conversion, status: ${status}, body: ${message}`);\n return undefined;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`Failed to send Meta conversion, network error: ${message}`);\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,cAAc;AAEpB,MAAM,0BAAoC,CAAC,IAAI;AAC/C,SAAS,iBAAiB,OAA+C;CACvE,MAAM,UAAU,OAAO,MAAM,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY;CACxD,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,OAAO,wBAAwB,SAAS,OAAO,IAAI,KAAA,IAAY;AACjE;;;;;AAMA,IAAI;;;;;;;AAQJ,SAAS,IAAI,OAA2B,UAAsC;CAC5E,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,QADe,iBAAiB,IAAIA,0BAAAA,aAAa,EAAA,CAAG,0BAA0B,OAAO,QAC1E,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,KAAA;AACpC;AAEA,MAAM,gBAAgB;;;;;;;AAQtB,SAAS,OACP,OACA,MACoB;CACpB,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,MAAM,UAAU,MAAM,KAAK,CAAC,CAAC,YAAY;CACzC,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,IAAI,cAAc,KAAK,OAAO,GAAG,OAAO;CACxC,MAAM,aAAa,KAAK,OAAO;CAC/B,IAAI,eAAe,KAAA,GAAW,OAAO,KAAA;CACrC,QAAA,GAAA,YAAA,WAAA,CAAkB,QAAQ,CAAC,CAAC,OAAO,UAAU,CAAC,CAAC,OAAO,KAAK;AAC7D;AAEA,MAAM,YAAY,MAAc;AAChC,MAAM,UAAU,MAAe,EAAE,UAAU,IAAI,IAAI,EAAE,MAAM,GAAG,CAAC;AAC/D,MAAM,iBAAiB,MAAc,EAAE,QAAQ,gBAAgB,EAAE;AACjE,MAAM,WAAW,MAAc;CAC7B,MAAM,MAAM,EAAE,QAAQ,OAAO,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;CAC/C,OAAO,IAAI,SAAS,IAAI,KAAA,IAAY;AACtC;;;;;AAMA,MAAM,UAAU,WACd,MAAM,KAAK,IAAI,IAAI,OAAO,QAAQ,SAAyB,SAAS,KAAA,CAAS,CAAC,CAAC;AAEjF,MAAM,QAAQ,WAAmC;CAC/C,MAAM,UAAU,OAAO,MAAM;CAC7B,OAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;AACxC;AA2BA,SAAS,YAAY,MAAiB,MAAwB,aAAmC;CAC/F,MAAM,WAAyB,CAAC;CAEhC,IAAI,KAAK,OAAO;EACd,MAAM,SAAS,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC,KAAK,KAAK;EACnE,SAAS,KAAK,KAAK,OAAO,KAAK,UAAU,IAAI,OAAOC,0BAAAA,cAAc,KAAK,CAAC,CAAC;CAC3E;CACA,IAAI,KAAK,cAAc;EACrB,MAAM,SAAS,MAAM,QAAQ,KAAK,YAAY,IAAI,KAAK,eAAe,CAAC,KAAK,YAAY;EACxF,SAAS,KAAK,KAAK,OAAO,KAAK,UAAU,IAAI,OAAOA,0BAAAA,cAAc,KAAK,CAAC,CAAC;CAC3E;CACA,IAAI,KAAK,WAAW,YAAY,KAAK,WAAW,QAC9C,SAAS,KAAK,KAAK,CAAC,IAAI,KAAK,WAAW,WAAW,MAAM,KAAKA,0BAAAA,cAAc,MAAM,CAAC,CAAC;CAEtF,IAAI,KAAK,SACP,IAAI,MAAM,QAAQ,KAAK,OAAO,GAAG;EAG/B,SAAS,KAAK,OAAO,KAAK,QAAQ,KAAK,MAAM,OAAO,EAAE,YAAY,QAAQ,CAAC,CAAC;EAC5E,SAAS,KAAK,OAAO,KAAK,QAAQ,KAAK,MAAM,OAAO,EAAE,WAAW,QAAQ,CAAC,CAAC;EAC3E,SAAS,KAAK,OAAO,KAAK,QAAQ,KAAK,MAAM,OAAO,EAAE,MAAM,aAAa,CAAC,CAAC;EAC3E,SAAS,KAAK,OAAO,KAAK,QAAQ,KAAK,MAAM,OAAO,EAAE,QAAQ,aAAa,CAAC,CAAC;EAC7E,SAAS,KAAK,OAAO,KAAK,QAAQ,KAAK,MAAM,OAAO,EAAE,aAAa,OAAO,CAAC,CAAC;EAC5E,SAAS,UAAU,OACjB,KAAK,QAAQ,KAAK,MAAM,IAAI,iBAAiB,EAAE,OAAO,GAAGA,0BAAAA,cAAc,OAAO,CAAC,CACjF;CACF,OAAO;EACL,SAAS,KAAK,KAAK,CAAC,OAAO,KAAK,QAAQ,YAAY,QAAQ,CAAC,CAAC;EAC9D,SAAS,KAAK,KAAK,CAAC,OAAO,KAAK,QAAQ,WAAW,QAAQ,CAAC,CAAC;EAC7D,SAAS,KAAK,KAAK,CAAC,OAAO,KAAK,QAAQ,MAAM,aAAa,CAAC,CAAC;EAC7D,SAAS,KAAK,KAAK,CAAC,OAAO,KAAK,QAAQ,QAAQ,aAAa,CAAC,CAAC;EAC/D,SAAS,KAAK,KAAK,CAAC,OAAO,KAAK,QAAQ,aAAa,OAAO,CAAC,CAAC;EAC9D,SAAS,UAAU,KAAK,CAAC,IAAI,iBAAiB,KAAK,QAAQ,OAAO,GAAGA,0BAAAA,cAAc,OAAO,CAAC,CAAC;EAG5F,SAAS,UAAU,OAAO,KAAK,QAAQ,YAAY,MAAM,GAAG,CAAC,GAAG,MAAM;EACtE,SAAS,SAAS,OAAO,KAAK,QAAQ,WAAW,MAAM,GAAG,CAAC,GAAG,MAAM;CACtE;CAEF,IAAI,KAAK,UAAU;EACjB,MAAM,EAAE,MAAM,OAAO,QAAQ,KAAK;EAClC,IAAI,UAAU,KAAK,KAAK,SAAS,CAAC,GAChC,SAAS,QAAA,GAAA,YAAA,WAAA,CAAkB,QAAQ,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,KAAK;EAE3E,IAAI,SAAS,KAAK,SAAS,IACzB,SAAS,QAAA,GAAA,YAAA,WAAA,CAAkB,QAAQ,CAAC,CAAC,OAAO,MAAM,SAAS,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK;EAE7F,IAAI,OAAO,KAAK,OAAO,IACrB,SAAS,QAAA,GAAA,YAAA,WAAA,CAAkB,QAAQ,CAAC,CAAC,OAAO,IAAI,SAAS,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK;CAE7F;CACA,IAAI,KAAK,WAAW,KAAK,QAAQ,WAAW,GAG1C,SAAS,cAAc,CAAC,KAAK,OAAO;CAEtC,IAAI,KAAK,YACP,SAAS,oBAAoB,KAAK;CAEpC,IAAI,KAAK,YACP,SAAS,oBAAoB,KAAK;CAEpC,IAAI,KAAK,aACP,SAAS,cAAc,KAAK;CAE9B,IAAI,KAAK,YACP,SAAS,UAAU,KAAK;CAG1B,IAAI,KAAK,KACP,SAAS,MAAM,KAAK;MACf,IAAI,KAAK,QACd,SAAS,MAAMC,uBAAAA,UAAU,KAAK,QAAQ,WAAW;CAEnD,IAAI,KAAK,KACP,SAAS,MAAM,KAAK;CAEtB,IAAI,KAAK,gBACP,SAAS,QAAQ,KAAK;CAExB,IAAI,KAAK,cAAc,OAAO,KAAK,eAAe,UAChD,SAAS,oBAAoB,KAAK;CAGpC,OAAO;AACT;AAEA,MAAM,sBAAsB;CAAC;CAAY;CAAY;AAAe;AAGpE,SAAS,cAAc,EAAE,MAAM,cAAwD;CACrF,MAAM,CAAC,GAAG,OAAO,qBAAqBC,kBAAAA,WAAW,MAAM,UAAU;CACjE,MAAM,EACJ,OACA,UACA,cACA,kBACA,aACA,UACA,cACA,eACA,WACA,eACA,QACA,mBACA,GAAG,sBACD;CAEJ,MAAM,aAAsC,CAAC;CAE7C,IAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,OAAO,SAAS,OAAO,WAAW,OAAO,KAAK,CAAC,CAAC,GAC3F,WAAW,QAAQ,OAAO,WAAW,OAAO,KAAK,CAAC;CAEpD,IAAI,UAGF,WAAW,WAAW,SAAS,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,QAAQ,WAAW,EAAE;CAE3E,IAAI,cAAc,WAAW,eAAe;CAC5C,IAAI,kBAAkB,WAAW,mBAAmB;CACpD,IAAI,aAAa,WAAW,cAAc;CAC1C,IAAI,UACF,WAAW,WAAW,SAAS,KAAK,MAAM;EACxC,MAAM,UAAmC,CAAC;EAC1C,IAAI,EAAE,IAAI,QAAQ,KAAK,EAAE;EACzB,IAAI,EAAE,UAAU,QAAQ,WAAW,EAAE;EACrC,IAAI,EAAE,YAAY,QAAQ,aAAa,EAAE;EACzC,IAAI,EAAE,OAAO,QAAQ,QAAQ,EAAE;EAC/B,IAAI,EAAE,aAAa,QAAQ,cAAc,EAAE;EAC3C,IAAI,EAAE,OAAO,QAAQ,QAAQ,EAAE;EAC/B,IAAI,EAAE,UAAU,QAAQ,WAAW,EAAE;EACrC,MAAM,WAAW,EAAE,mBAAmB,KAAK,CAAC,CAAC,YAAY;EACzD,IAAI,YAAY,oBAAoB,SAAS,QAAQ,GAAG,QAAQ,oBAAoB;EACpF,OAAO;CACT,CAAC;CAEH,IAAI,cAAc,WAAW,eAAe;CAC5C,IAAI,eAAe,WAAW,gBAAgB;CAC9C,IAAI,WAAW,WAAW,YAAY;CACtC,IAAI,eAAe,WAAW,gBAAgB;CAC9C,IAAI,QAAQ,WAAW,SAAS,OAAO,SAAS;CAChD,MAAM,WAAW,mBAAmB,KAAK,CAAC,CAAC,YAAY;CACvD,IAAI,YAAY,oBAAoB,SAAS,QAAQ,GAAG,WAAW,oBAAoB;CAEvF,OAAO;EAAE,GAAG;EAAY,GAAG;CAAkB;AAC/C;;;;;;;AAQA,SAAS,WAAW,MAAiB,gBAAiD;CACpF,MAAM,UAA2C,CAAC;CAClD,IAAI,KAAK,YAAY,SAAS,KAAK,YAAY,UAC7C,QAAQ,KAAK;MACR,IAAI,KAAK,YAAY,WAC1B,QAAQ,KAAK;CAEf,QAAQ,KAAK;CACb,MAAM,eAAe,KAAK,SAAS,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC;CAClD,IAAI,cAAc,QAAQ,KAAK;CAC/B,IAAI,KAAK,SAAS,QAAQ,KAAK,KAAK;CACpC,IAAI,KAAK,YAAY,QAAQ,KAAK,KAAK;CACvC,IAAI,KAAK,iBAAiB,QAAQ,KAAK,KAAK;CAC5C,IAAI,KAAK,UAAU,QAAQ,KAAK,KAAK;CACrC,IAAI,KAAK,cAAc,QAAQ,KAAK,KAAK;CACzC,IAAI,KAAK,eAAe,QAAQ,MAAM,KAAK;CAC3C,IAAI,KAAK,oBAAoB,QAAQ,MAAM,KAAK,mBAAmB,SAAS;CAE5E,MAAM,UAAmC,EAAE,QAAQ;CACnD,IAAI,KAAK,gBAAgB,QAAQ,8BAA8B;CAC/D,IAAI,KAAK,kBAAkB,QAAQ,mBAAmB,KAAK;CAC3D,OAAO;AACT;;;;;;;AAoBA,SAAgB,aAEd,OACA,MACA,gBACW;CACX,MAAM,cAAc,IAAI,KAAK,MAAM,UAAU,CAAC,CAAC,QAAQ;CACvD,MAAM,CAAC,GAAG,aAAaA,kBAAAA,WAAW,MAAM,MAAM,MAAM,UAAU;CAC9D,MAAM,SAASC,6BAAAA,oBAAoB,MAAM,QAAQ;CAEjD,MAAM,YAAuB;EAC3B,YAAY;EACZ,YAAY,KAAK,MAAM,cAAc,GAAI;EACzC,UAAU,MAAM,KAAK,mBAAmB,MAAM;EAG9C,eAAe,WAAW,QAAQ,QAAQ,WAAW,QAAQ,YAAY;EACzE,WAAW,YAAY,MAAM,MAAM,MAAM,WAAW;EACpD,aAAa,cAAc,KAAK;CAClC;CACA,IAAI,WAAW,SAAS,gBACtB,UAAU,WAAW,WAAW,MAAM,MAAM,cAAc;CAE5D,MAAM,iBAAiBC,6BAAAA,aAAa,MAAM,IAAI;CAC9C,IAAI,gBACF,UAAU,mBAAmB;CAE/B,OAAO;AACT;;;;;;AAsBA,eAAsB,oBACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,UAAkC,CAAC,GACW;CAC9C,MAAM,aAAa,OAChB,QAAQ,UAAU,CAACC,qCAAAA,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACvD,KAAK,UAAU,aAAa,OAAO,MAAM,QAAQ,cAAc,CAAC;CACnE,IAAI,WAAW,WAAW,GAAG,OAAO,KAAA;CAEpC,MAAM,UAAU,QAAQ,cAAc;CACtC,IAAI;EACF,MAAM,WAAW,OAAA,GAAA,cAAA,MAAA,CAAY,8BAA8B,QAAQ,GAAG,QAAQ,UAAU;GACtF,QAAQ;GACR,SAAS;IAAE,gBAAgB;IAAoB,QAAQ;GAAmB;GAC1E,MAAM,KAAK,UAAU;IACnB,MAAM;IACN,iBAAiB,QAAQ;IACzB,cAAc;GAChB,CAAC;EACH,CAAC;EACD,IAAI,SAAS,IAAI,OAAQ,MAAM,SAAS,KAAK;EAC7C,MAAM,EAAE,WAAW;EACnB,MAAM,UAAU,MAAM,SAAS,KAAK;EACpC,QAAQ,MAAM,2CAA2C,OAAO,UAAU,SAAS;EACnF;CACF,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,QAAQ,MAAM,kDAAkD,SAAS;EACzE;CACF;AACF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { TrackEvent, UserProvidedData } from "../track/types.cjs";
|
|
2
|
+
//#region src/server/meta-capi.d.ts
|
|
3
|
+
interface CapiUserData {
|
|
4
|
+
em?: string[];
|
|
5
|
+
ph?: string[];
|
|
6
|
+
ge?: string[];
|
|
7
|
+
fn?: string[];
|
|
8
|
+
ln?: string[];
|
|
9
|
+
ct?: string[];
|
|
10
|
+
st?: string[];
|
|
11
|
+
zp?: string[];
|
|
12
|
+
country?: string[];
|
|
13
|
+
external_id?: string[];
|
|
14
|
+
client_ip_address?: string;
|
|
15
|
+
client_user_agent?: string;
|
|
16
|
+
fbc?: string;
|
|
17
|
+
fbp?: string;
|
|
18
|
+
fb_login_id?: string;
|
|
19
|
+
page_id?: string;
|
|
20
|
+
f5first?: string;
|
|
21
|
+
f5last?: string;
|
|
22
|
+
dobd?: string;
|
|
23
|
+
dobm?: string;
|
|
24
|
+
doby?: string;
|
|
25
|
+
madid?: string;
|
|
26
|
+
}
|
|
27
|
+
/** One entry of the `data` array POSTed to `/{pixel_id}/events`. */
|
|
28
|
+
interface CapiEvent {
|
|
29
|
+
event_name: string;
|
|
30
|
+
event_time: number;
|
|
31
|
+
event_id: string;
|
|
32
|
+
event_source_url?: string;
|
|
33
|
+
action_source: 'app' | 'website' | 'other';
|
|
34
|
+
user_data: CapiUserData;
|
|
35
|
+
custom_data: Record<string, unknown>;
|
|
36
|
+
app_data?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `action_source` is derived from `event.platform` alone: an OS platform is an app, `web` is a
|
|
40
|
+
* website, and everything else — including a backend-built offline conversion, which should
|
|
41
|
+
* declare `platform: 'unknown'` — lands in Meta's `other`, exactly where the business-SDK
|
|
42
|
+
* sender put an explicit `'offline'` too.
|
|
43
|
+
*/
|
|
44
|
+
declare function getCapiEvent(event: TrackEvent<any>, data: UserProvidedData, appPackageName?: string): CapiEvent;
|
|
45
|
+
interface MetaConversionsOptions {
|
|
46
|
+
/** Attaches `app_data`/extinfo to events whose platform resolves to an app. */
|
|
47
|
+
appPackageName?: string;
|
|
48
|
+
/** Routes the batch to Events Manager's Test Events tab instead of production. */
|
|
49
|
+
testEventCode?: string;
|
|
50
|
+
/** Graph API version, e.g. 'v24.0'. */
|
|
51
|
+
apiVersion?: string;
|
|
52
|
+
}
|
|
53
|
+
interface MetaConversionsResponse {
|
|
54
|
+
events_received?: number;
|
|
55
|
+
messages?: string[];
|
|
56
|
+
fbtrace_id?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The lightweight counterpart to `sendMetaEvents`: same filtering, same payload, same
|
|
60
|
+
* never-throws contract, minus the 31MB SDK. The token travels in the JSON body, never in the
|
|
61
|
+
* URL, and is never logged.
|
|
62
|
+
*/
|
|
63
|
+
declare function sendMetaConversions(accessToken: string, pixelId: string, events: TrackEvent<any>[], data?: UserProvidedData, options?: MetaConversionsOptions): Promise<MetaConversionsResponse | undefined>;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendMetaConversions };
|
|
66
|
+
//# sourceMappingURL=meta-capi.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta-capi.d.cts","names":[],"sources":["../../src/server/meta-capi.ts"],"mappings":";;UAqGU;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;UAqLe;EACf;EACA;EACA;EACA;EACA;EACA,WAAW;EACX,aAAa;EACb,WAAW;;;;;;;;iBASG,aAEd,OAAO,iBACP,MAAM,kBACN,0BACC;UAyBc;;EAEf;;EAEA;;EAEA;;UAGe;EACf;EACA;EACA;;;;;;;iBAQoB,oBACpB,qBACA,iBAEA,QAAQ,mBACR,OAAM,kBACN,UAAS,yBACR,QAAQ"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { TrackEvent, UserProvidedData } from "../track/types.mjs";
|
|
2
|
+
//#region src/server/meta-capi.d.ts
|
|
3
|
+
interface CapiUserData {
|
|
4
|
+
em?: string[];
|
|
5
|
+
ph?: string[];
|
|
6
|
+
ge?: string[];
|
|
7
|
+
fn?: string[];
|
|
8
|
+
ln?: string[];
|
|
9
|
+
ct?: string[];
|
|
10
|
+
st?: string[];
|
|
11
|
+
zp?: string[];
|
|
12
|
+
country?: string[];
|
|
13
|
+
external_id?: string[];
|
|
14
|
+
client_ip_address?: string;
|
|
15
|
+
client_user_agent?: string;
|
|
16
|
+
fbc?: string;
|
|
17
|
+
fbp?: string;
|
|
18
|
+
fb_login_id?: string;
|
|
19
|
+
page_id?: string;
|
|
20
|
+
f5first?: string;
|
|
21
|
+
f5last?: string;
|
|
22
|
+
dobd?: string;
|
|
23
|
+
dobm?: string;
|
|
24
|
+
doby?: string;
|
|
25
|
+
madid?: string;
|
|
26
|
+
}
|
|
27
|
+
/** One entry of the `data` array POSTed to `/{pixel_id}/events`. */
|
|
28
|
+
interface CapiEvent {
|
|
29
|
+
event_name: string;
|
|
30
|
+
event_time: number;
|
|
31
|
+
event_id: string;
|
|
32
|
+
event_source_url?: string;
|
|
33
|
+
action_source: 'app' | 'website' | 'other';
|
|
34
|
+
user_data: CapiUserData;
|
|
35
|
+
custom_data: Record<string, unknown>;
|
|
36
|
+
app_data?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `action_source` is derived from `event.platform` alone: an OS platform is an app, `web` is a
|
|
40
|
+
* website, and everything else — including a backend-built offline conversion, which should
|
|
41
|
+
* declare `platform: 'unknown'` — lands in Meta's `other`, exactly where the business-SDK
|
|
42
|
+
* sender put an explicit `'offline'` too.
|
|
43
|
+
*/
|
|
44
|
+
declare function getCapiEvent(event: TrackEvent<any>, data: UserProvidedData, appPackageName?: string): CapiEvent;
|
|
45
|
+
interface MetaConversionsOptions {
|
|
46
|
+
/** Attaches `app_data`/extinfo to events whose platform resolves to an app. */
|
|
47
|
+
appPackageName?: string;
|
|
48
|
+
/** Routes the batch to Events Manager's Test Events tab instead of production. */
|
|
49
|
+
testEventCode?: string;
|
|
50
|
+
/** Graph API version, e.g. 'v24.0'. */
|
|
51
|
+
apiVersion?: string;
|
|
52
|
+
}
|
|
53
|
+
interface MetaConversionsResponse {
|
|
54
|
+
events_received?: number;
|
|
55
|
+
messages?: string[];
|
|
56
|
+
fbtrace_id?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The lightweight counterpart to `sendMetaEvents`: same filtering, same payload, same
|
|
60
|
+
* never-throws contract, minus the 31MB SDK. The token travels in the JSON body, never in the
|
|
61
|
+
* URL, and is never logged.
|
|
62
|
+
*/
|
|
63
|
+
declare function sendMetaConversions(accessToken: string, pixelId: string, events: TrackEvent<any>[], data?: UserProvidedData, options?: MetaConversionsOptions): Promise<MetaConversionsResponse | undefined>;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendMetaConversions };
|
|
66
|
+
//# sourceMappingURL=meta-capi.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta-capi.d.mts","names":[],"sources":["../../src/server/meta-capi.ts"],"mappings":";;UAqGU;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;UAqLe;EACf;EACA;EACA;EACA;EACA;EACA,WAAW;EACX,aAAa;EACb,WAAW;;;;;;;;iBASG,aAEd,OAAO,iBACP,MAAM,kBACN,0BACC;UAyBc;;EAEf;;EAEA;;EAEA;;UAGe;EACf;EACA;EACA;;;;;;;iBAQoB,oBACpB,qBACA,iBAEA,QAAQ,mBACR,OAAM,kBACN,UAAS,yBACR,QAAQ"}
|