@shware/analytics 7.6.0 → 8.0.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/server/index.cjs +1 -3
- package/dist/server/index.d.cts +2 -3
- package/dist/server/index.d.mts +2 -3
- package/dist/server/index.mjs +2 -3
- package/dist/server/legacy.cjs +3 -0
- package/dist/server/legacy.d.cts +2 -0
- package/dist/server/legacy.d.mts +2 -0
- package/dist/server/legacy.mjs +2 -0
- package/dist/server/meta-capi.cjs +9 -8
- package/dist/server/meta-capi.cjs.map +1 -1
- package/dist/server/meta-capi.d.cts +5 -5
- package/dist/server/meta-capi.d.cts.map +1 -1
- package/dist/server/meta-capi.d.mts +5 -5
- package/dist/server/meta-capi.d.mts.map +1 -1
- package/dist/server/meta-capi.mjs +9 -8
- package/dist/server/meta-capi.mjs.map +1 -1
- package/dist/server/meta-conversions-api.cjs +1 -1
- package/dist/server/meta-conversions-api.mjs +1 -1
- package/package.json +11 -1
package/dist/next/index.cjs
CHANGED
|
@@ -4,11 +4,11 @@ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
|
4
4
|
const require_track_index = require("../track/index.cjs");
|
|
5
5
|
const require_hooks_use_outbound_click_analytics = require("../hooks/use-outbound-click-analytics.cjs");
|
|
6
6
|
const require_hooks_use_web_analytics = require("../hooks/use-web-analytics.cjs");
|
|
7
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
7
|
let next_navigation_js = require("next/navigation.js");
|
|
9
8
|
let next_script_js = require("next/script.js");
|
|
10
9
|
next_script_js = require_runtime.__toESM(next_script_js, 1);
|
|
11
10
|
let next_web_vitals_js = require("next/web-vitals.js");
|
|
11
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
12
|
//#region src/next/index.tsx
|
|
13
13
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, hotjarId, redditPixelId, linkedInPartnerId, facebookAppId, reportWebVitals = true }) {
|
|
14
14
|
require_hooks_use_web_analytics.useWebAnalytics((0, next_navigation_js.usePathname)());
|
package/dist/next/index.mjs
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { track } from "../track/index.mjs";
|
|
3
3
|
import { useOutboundClickAnalytics } from "../hooks/use-outbound-click-analytics.mjs";
|
|
4
4
|
import { useWebAnalytics } from "../hooks/use-web-analytics.mjs";
|
|
5
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
5
|
import { usePathname } from "next/navigation.js";
|
|
7
6
|
import Script from "next/script.js";
|
|
8
7
|
import { useReportWebVitals } from "next/web-vitals.js";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/next/index.tsx
|
|
10
10
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, hotjarId, redditPixelId, linkedInPartnerId, facebookAppId, reportWebVitals = true }) {
|
|
11
11
|
useWebAnalytics(usePathname());
|
|
@@ -3,8 +3,8 @@ const require_track_index = require("../track/index.cjs");
|
|
|
3
3
|
const require_hooks_use_outbound_click_analytics = require("../hooks/use-outbound-click-analytics.cjs");
|
|
4
4
|
const require_hooks_use_report_web_vitals = require("../hooks/use-report-web-vitals.cjs");
|
|
5
5
|
const require_hooks_use_web_analytics = require("../hooks/use-web-analytics.cjs");
|
|
6
|
-
let react_router = require("react-router");
|
|
7
6
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
|
+
let react_router = require("react-router");
|
|
8
8
|
//#region src/react-router/index.tsx
|
|
9
9
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, redditPixelId, linkedInPartnerId, hotjarId, facebookAppId, reportWebVitals = true }) {
|
|
10
10
|
const { pathname } = (0, react_router.useLocation)();
|
|
@@ -2,8 +2,8 @@ import { track } from "../track/index.mjs";
|
|
|
2
2
|
import { useOutboundClickAnalytics } from "../hooks/use-outbound-click-analytics.mjs";
|
|
3
3
|
import { useReportWebVitals } from "../hooks/use-report-web-vitals.mjs";
|
|
4
4
|
import { useWebAnalytics } from "../hooks/use-web-analytics.mjs";
|
|
5
|
-
import { useLocation } from "react-router";
|
|
6
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useLocation } from "react-router";
|
|
7
7
|
//#region src/react-router/index.tsx
|
|
8
8
|
function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, redditPixelId, linkedInPartnerId, hotjarId, facebookAppId, reportWebVitals = true }) {
|
|
9
9
|
const { pathname } = useLocation();
|
package/dist/server/index.cjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_click_id_index = require("../click-id/index.cjs");
|
|
3
|
-
const require_server_meta_conversions_api = require("./meta-conversions-api.cjs");
|
|
4
3
|
const require_server_meta_capi = require("./meta-capi.cjs");
|
|
5
4
|
const require_server_reddit_conversions_api = require("./reddit-conversions-api.cjs");
|
|
6
5
|
const require_server_linkedin_conversions_api = require("./linkedin-conversions-api.cjs");
|
|
@@ -12,8 +11,7 @@ exports.getCapiEvent = require_server_meta_capi.getCapiEvent;
|
|
|
12
11
|
exports.parseFbc = require_click_id_index.parseFbc;
|
|
13
12
|
exports.resolveClickIdCookies = require_click_id_index.resolveClickIdCookies;
|
|
14
13
|
exports.sendLinkedinEvents = require_server_linkedin_conversions_api.sendEvents;
|
|
15
|
-
exports.
|
|
16
|
-
exports.sendMetaEvents = require_server_meta_conversions_api.sendEvents;
|
|
14
|
+
exports.sendMetaEvents = require_server_meta_capi.sendEvents;
|
|
17
15
|
exports.sendOpenAIEvents = require_server_openai_conversions_api.sendEvents;
|
|
18
16
|
exports.sendRedditEvents = require_server_reddit_conversions_api.sendEvents;
|
|
19
17
|
exports.toSetCookieHeaders = require_click_id_index.toSetCookieHeaders;
|
package/dist/server/index.d.cts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { FBC_COOKIE, ParsedFbc, RDT_CID_COOKIE, ResolveClickIdCookiesInput, ResolveClickIdCookiesResult, formatFbc, parseFbc, resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.cjs";
|
|
2
2
|
import { EventActionSource } from "./action-source.cjs";
|
|
3
|
-
import { sendEvents as sendEvents$1 } from "./meta-
|
|
4
|
-
import { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendMetaConversions } from "./meta-capi.cjs";
|
|
3
|
+
import { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendEvents as sendEvents$1 } from "./meta-capi.cjs";
|
|
5
4
|
import { sendEvents as sendEvents$3 } from "./reddit-conversions-api.cjs";
|
|
6
5
|
import { LinkedinConversionConfig, sendEvents } from "./linkedin-conversions-api.cjs";
|
|
7
6
|
import { sendEvents as sendEvents$2 } from "./openai-conversions-api.cjs";
|
|
8
|
-
export { type CapiEvent, type EventActionSource, FBC_COOKIE, type LinkedinConversionConfig, type MetaConversionsOptions, type MetaConversionsResponse, type ParsedFbc, RDT_CID_COOKIE, type ResolveClickIdCookiesInput, type ResolveClickIdCookiesResult, formatFbc, getCapiEvent, parseFbc, resolveClickIdCookies, sendEvents as sendLinkedinEvents,
|
|
7
|
+
export { type CapiEvent, type EventActionSource, FBC_COOKIE, type LinkedinConversionConfig, type MetaConversionsOptions, type MetaConversionsResponse, type ParsedFbc, RDT_CID_COOKIE, type ResolveClickIdCookiesInput, type ResolveClickIdCookiesResult, formatFbc, getCapiEvent, parseFbc, resolveClickIdCookies, sendEvents as sendLinkedinEvents, sendEvents$1 as sendMetaEvents, sendEvents$2 as sendOpenAIEvents, sendEvents$3 as sendRedditEvents, toSetCookieHeaders };
|
package/dist/server/index.d.mts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { FBC_COOKIE, ParsedFbc, RDT_CID_COOKIE, ResolveClickIdCookiesInput, ResolveClickIdCookiesResult, formatFbc, parseFbc, resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.mjs";
|
|
2
2
|
import { EventActionSource } from "./action-source.mjs";
|
|
3
|
-
import { sendEvents as sendEvents$1 } from "./meta-
|
|
4
|
-
import { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendMetaConversions } from "./meta-capi.mjs";
|
|
3
|
+
import { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendEvents as sendEvents$1 } from "./meta-capi.mjs";
|
|
5
4
|
import { sendEvents as sendEvents$3 } from "./reddit-conversions-api.mjs";
|
|
6
5
|
import { LinkedinConversionConfig, sendEvents } from "./linkedin-conversions-api.mjs";
|
|
7
6
|
import { sendEvents as sendEvents$2 } from "./openai-conversions-api.mjs";
|
|
8
|
-
export { type CapiEvent, type EventActionSource, FBC_COOKIE, type LinkedinConversionConfig, type MetaConversionsOptions, type MetaConversionsResponse, type ParsedFbc, RDT_CID_COOKIE, type ResolveClickIdCookiesInput, type ResolveClickIdCookiesResult, formatFbc, getCapiEvent, parseFbc, resolveClickIdCookies, sendEvents as sendLinkedinEvents,
|
|
7
|
+
export { type CapiEvent, type EventActionSource, FBC_COOKIE, type LinkedinConversionConfig, type MetaConversionsOptions, type MetaConversionsResponse, type ParsedFbc, RDT_CID_COOKIE, type ResolveClickIdCookiesInput, type ResolveClickIdCookiesResult, formatFbc, getCapiEvent, parseFbc, resolveClickIdCookies, sendEvents as sendLinkedinEvents, sendEvents$1 as sendMetaEvents, sendEvents$2 as sendOpenAIEvents, sendEvents$3 as sendRedditEvents, toSetCookieHeaders };
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { FBC_COOKIE, RDT_CID_COOKIE, formatFbc, parseFbc, resolveClickIdCookies, toSetCookieHeaders } from "../click-id/index.mjs";
|
|
2
|
-
import { sendEvents as sendEvents$1 } from "./meta-
|
|
3
|
-
import { getCapiEvent, sendMetaConversions } from "./meta-capi.mjs";
|
|
2
|
+
import { getCapiEvent, sendEvents as sendEvents$1 } from "./meta-capi.mjs";
|
|
4
3
|
import { sendEvents as sendEvents$3 } from "./reddit-conversions-api.mjs";
|
|
5
4
|
import { sendEvents } from "./linkedin-conversions-api.mjs";
|
|
6
5
|
import { sendEvents as sendEvents$2 } from "./openai-conversions-api.mjs";
|
|
7
|
-
export { FBC_COOKIE, RDT_CID_COOKIE, formatFbc, getCapiEvent, parseFbc, resolveClickIdCookies, sendEvents as sendLinkedinEvents,
|
|
6
|
+
export { FBC_COOKIE, RDT_CID_COOKIE, formatFbc, getCapiEvent, parseFbc, resolveClickIdCookies, sendEvents as sendLinkedinEvents, sendEvents$1 as sendMetaEvents, sendEvents$2 as sendOpenAIEvents, sendEvents$3 as sendRedditEvents, toSetCookieHeaders };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_third_parties_ignored_events = require("../third-parties/ignored-events.cjs");
|
|
3
3
|
const require_click_id_index = require("../click-id/index.cjs");
|
|
4
|
-
const require_server_action_source = require("./action-source.cjs");
|
|
5
4
|
const require_track_fbq = require("../track/fbq.cjs");
|
|
5
|
+
const require_server_action_source = require("./action-source.cjs");
|
|
6
6
|
const require_server_page_location = require("./page-location.cjs");
|
|
7
7
|
let _shware_utils = require("@shware/utils");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
@@ -10,8 +10,9 @@ let capi_param_builder_nodejs = require("capi-param-builder-nodejs");
|
|
|
10
10
|
//#region src/server/meta-capi.ts
|
|
11
11
|
/**
|
|
12
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.
|
|
14
|
-
*
|
|
13
|
+
* the 31MB `facebook-nodejs-business-sdk` with ~0.5MB of parameter building. The SDK-based
|
|
14
|
+
* sender survives untouched at `@shware/analytics/server/legacy`; this module is its drop-in
|
|
15
|
+
* successor.
|
|
15
16
|
*
|
|
16
17
|
* The wire payload is byte-identical to what the business SDK's `ServerEvent.normalize()`
|
|
17
18
|
* produces — enforced by a differential test that runs both builders over the same events.
|
|
@@ -219,11 +220,11 @@ function getCapiEvent(event, data, appPackageName) {
|
|
|
219
220
|
return capiEvent;
|
|
220
221
|
}
|
|
221
222
|
/**
|
|
222
|
-
* The lightweight counterpart to
|
|
223
|
-
* never-throws contract, minus the 31MB SDK. The token
|
|
224
|
-
* URL, and is never logged.
|
|
223
|
+
* The lightweight counterpart to the legacy SDK sender (`@shware/analytics/server/legacy`):
|
|
224
|
+
* same filtering, same payload, same never-throws contract, minus the 31MB SDK. The token
|
|
225
|
+
* travels in the JSON body, never in the URL, and is never logged.
|
|
225
226
|
*/
|
|
226
|
-
async function
|
|
227
|
+
async function sendEvents(accessToken, pixelId, events, data = {}, options = {}) {
|
|
227
228
|
const capiEvents = events.filter((event) => !require_third_parties_ignored_events.IGNORED_EVENTS.includes(event.name)).map((event) => getCapiEvent(event, data, options.appPackageName));
|
|
228
229
|
if (capiEvents.length === 0) return void 0;
|
|
229
230
|
const version = options.apiVersion ?? API_VERSION;
|
|
@@ -253,6 +254,6 @@ async function sendMetaConversions(accessToken, pixelId, events, data = {}, opti
|
|
|
253
254
|
}
|
|
254
255
|
//#endregion
|
|
255
256
|
exports.getCapiEvent = getCapiEvent;
|
|
256
|
-
exports.
|
|
257
|
+
exports.sendEvents = sendEvents;
|
|
257
258
|
|
|
258
259
|
//# sourceMappingURL=meta-capi.cjs.map
|
|
@@ -1 +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":"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"}
|
|
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. The SDK-based\n * sender survives untouched at `@shware/analytics/server/legacy`; this module is its drop-in\n * successor.\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\n .trim()\n .toUpperCase()\n .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 the legacy SDK sender (`@shware/analytics/server/legacy`):\n * same filtering, same payload, same never-throws contract, minus the 31MB SDK. The token\n * travels in the JSON body, never in the URL, and is never logged.\n */\nexport async function sendEvents(\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":"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"}
|
|
@@ -56,11 +56,11 @@ interface MetaConversionsResponse {
|
|
|
56
56
|
fbtrace_id?: string;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* The lightweight counterpart to
|
|
60
|
-
* never-throws contract, minus the 31MB SDK. The token
|
|
61
|
-
* URL, and is never logged.
|
|
59
|
+
* The lightweight counterpart to the legacy SDK sender (`@shware/analytics/server/legacy`):
|
|
60
|
+
* same filtering, same payload, same never-throws contract, minus the 31MB SDK. The token
|
|
61
|
+
* travels in the JSON body, never in the URL, and is never logged.
|
|
62
62
|
*/
|
|
63
|
-
declare function
|
|
63
|
+
declare function sendEvents(accessToken: string, pixelId: string, events: TrackEvent<any>[], data?: UserProvidedData, options?: MetaConversionsOptions): Promise<MetaConversionsResponse | undefined>;
|
|
64
64
|
//#endregion
|
|
65
|
-
export { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent,
|
|
65
|
+
export { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendEvents };
|
|
66
66
|
//# sourceMappingURL=meta-capi.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta-capi.d.cts","names":[],"sources":["../../src/server/meta-capi.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"meta-capi.d.cts","names":[],"sources":["../../src/server/meta-capi.ts"],"mappings":";;UAsGU;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;UAwLe;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,WACpB,qBACA,iBAEA,QAAQ,mBACR,OAAM,kBACN,UAAS,yBACR,QAAQ"}
|
|
@@ -56,11 +56,11 @@ interface MetaConversionsResponse {
|
|
|
56
56
|
fbtrace_id?: string;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* The lightweight counterpart to
|
|
60
|
-
* never-throws contract, minus the 31MB SDK. The token
|
|
61
|
-
* URL, and is never logged.
|
|
59
|
+
* The lightweight counterpart to the legacy SDK sender (`@shware/analytics/server/legacy`):
|
|
60
|
+
* same filtering, same payload, same never-throws contract, minus the 31MB SDK. The token
|
|
61
|
+
* travels in the JSON body, never in the URL, and is never logged.
|
|
62
62
|
*/
|
|
63
|
-
declare function
|
|
63
|
+
declare function sendEvents(accessToken: string, pixelId: string, events: TrackEvent<any>[], data?: UserProvidedData, options?: MetaConversionsOptions): Promise<MetaConversionsResponse | undefined>;
|
|
64
64
|
//#endregion
|
|
65
|
-
export { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent,
|
|
65
|
+
export { CapiEvent, MetaConversionsOptions, MetaConversionsResponse, getCapiEvent, sendEvents };
|
|
66
66
|
//# sourceMappingURL=meta-capi.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta-capi.d.mts","names":[],"sources":["../../src/server/meta-capi.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"meta-capi.d.mts","names":[],"sources":["../../src/server/meta-capi.ts"],"mappings":";;UAsGU;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;UAwLe;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,WACpB,qBACA,iBAEA,QAAQ,mBACR,OAAM,kBACN,UAAS,yBACR,QAAQ"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
|
|
2
2
|
import { formatFbc } from "../click-id/index.mjs";
|
|
3
|
-
import { resolveActionSource } from "./action-source.mjs";
|
|
4
3
|
import { mapFBEvent } from "../track/fbq.mjs";
|
|
4
|
+
import { resolveActionSource } from "./action-source.mjs";
|
|
5
5
|
import { pageLocation } from "./page-location.mjs";
|
|
6
6
|
import { fetch } from "@shware/utils";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
@@ -9,8 +9,9 @@ import { PII_DATA_TYPE, ParamBuilder } from "capi-param-builder-nodejs";
|
|
|
9
9
|
//#region src/server/meta-capi.ts
|
|
10
10
|
/**
|
|
11
11
|
* Meta Conversions API sender built on `capi-param-builder-nodejs` and plain `fetch`, replacing
|
|
12
|
-
* the 31MB `facebook-nodejs-business-sdk` with ~0.5MB of parameter building.
|
|
13
|
-
*
|
|
12
|
+
* the 31MB `facebook-nodejs-business-sdk` with ~0.5MB of parameter building. The SDK-based
|
|
13
|
+
* sender survives untouched at `@shware/analytics/server/legacy`; this module is its drop-in
|
|
14
|
+
* successor.
|
|
14
15
|
*
|
|
15
16
|
* The wire payload is byte-identical to what the business SDK's `ServerEvent.normalize()`
|
|
16
17
|
* produces — enforced by a differential test that runs both builders over the same events.
|
|
@@ -218,11 +219,11 @@ function getCapiEvent(event, data, appPackageName) {
|
|
|
218
219
|
return capiEvent;
|
|
219
220
|
}
|
|
220
221
|
/**
|
|
221
|
-
* The lightweight counterpart to
|
|
222
|
-
* never-throws contract, minus the 31MB SDK. The token
|
|
223
|
-
* URL, and is never logged.
|
|
222
|
+
* The lightweight counterpart to the legacy SDK sender (`@shware/analytics/server/legacy`):
|
|
223
|
+
* same filtering, same payload, same never-throws contract, minus the 31MB SDK. The token
|
|
224
|
+
* travels in the JSON body, never in the URL, and is never logged.
|
|
224
225
|
*/
|
|
225
|
-
async function
|
|
226
|
+
async function sendEvents(accessToken, pixelId, events, data = {}, options = {}) {
|
|
226
227
|
const capiEvents = events.filter((event) => !IGNORED_EVENTS.includes(event.name)).map((event) => getCapiEvent(event, data, options.appPackageName));
|
|
227
228
|
if (capiEvents.length === 0) return void 0;
|
|
228
229
|
const version = options.apiVersion ?? API_VERSION;
|
|
@@ -251,6 +252,6 @@ async function sendMetaConversions(accessToken, pixelId, events, data = {}, opti
|
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
//#endregion
|
|
254
|
-
export { getCapiEvent,
|
|
255
|
+
export { getCapiEvent, sendEvents };
|
|
255
256
|
|
|
256
257
|
//# sourceMappingURL=meta-capi.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta-capi.mjs","names":[],"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":"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"}
|
|
1
|
+
{"version":3,"file":"meta-capi.mjs","names":[],"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. The SDK-based\n * sender survives untouched at `@shware/analytics/server/legacy`; this module is its drop-in\n * successor.\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\n .trim()\n .toUpperCase()\n .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 the legacy SDK sender (`@shware/analytics/server/legacy`):\n * same filtering, same payload, same never-throws contract, minus the 31MB SDK. The token\n * travels in the JSON body, never in the URL, and is never logged.\n */\nexport async function sendEvents(\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":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_third_parties_ignored_events = require("../third-parties/ignored-events.cjs");
|
|
3
3
|
const require_click_id_index = require("../click-id/index.cjs");
|
|
4
|
-
const require_server_action_source = require("./action-source.cjs");
|
|
5
4
|
const require_track_fbq = require("../track/fbq.cjs");
|
|
5
|
+
const require_server_action_source = require("./action-source.cjs");
|
|
6
6
|
const require_server_page_location = require("./page-location.cjs");
|
|
7
7
|
let facebook_nodejs_business_sdk = require("facebook-nodejs-business-sdk");
|
|
8
8
|
//#region src/server/meta-conversions-api.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
|
|
2
2
|
import { formatFbc } from "../click-id/index.mjs";
|
|
3
|
-
import { resolveActionSource } from "./action-source.mjs";
|
|
4
3
|
import { mapFBEvent } from "../track/fbq.mjs";
|
|
4
|
+
import { resolveActionSource } from "./action-source.mjs";
|
|
5
5
|
import { pageLocation } from "./page-location.mjs";
|
|
6
6
|
import { AppData, Content, CustomData, EventRequest, ExtendedDeviceInfo, ServerEvent, UserData } from "facebook-nodejs-business-sdk";
|
|
7
7
|
//#region src/server/meta-conversions-api.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shware/analytics",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -120,6 +120,16 @@
|
|
|
120
120
|
"types": "./dist/server/index.d.cts",
|
|
121
121
|
"default": "./dist/server/index.cjs"
|
|
122
122
|
}
|
|
123
|
+
},
|
|
124
|
+
"./server/legacy": {
|
|
125
|
+
"import": {
|
|
126
|
+
"types": "./dist/server/legacy.d.mts",
|
|
127
|
+
"default": "./dist/server/legacy.mjs"
|
|
128
|
+
},
|
|
129
|
+
"require": {
|
|
130
|
+
"types": "./dist/server/legacy.d.cts",
|
|
131
|
+
"default": "./dist/server/legacy.cjs"
|
|
132
|
+
}
|
|
123
133
|
}
|
|
124
134
|
},
|
|
125
135
|
"publishConfig": {
|