@shware/analytics 9.0.1 → 9.1.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/server/google-data-manager.cjs +3 -3
- package/dist/server/google-data-manager.cjs.map +1 -1
- package/dist/server/google-data-manager.d.cts +15 -2
- package/dist/server/google-data-manager.d.cts.map +1 -1
- package/dist/server/google-data-manager.d.mts +15 -2
- package/dist/server/google-data-manager.d.mts.map +1 -1
- package/dist/server/google-data-manager.mjs +3 -3
- package/dist/server/google-data-manager.mjs.map +1 -1
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.mts +2 -2
- package/dist/server/linkedin-conversions-api.cjs +6 -1
- package/dist/server/linkedin-conversions-api.cjs.map +1 -1
- package/dist/server/linkedin-conversions-api.d.cts.map +1 -1
- package/dist/server/linkedin-conversions-api.d.mts.map +1 -1
- package/dist/server/linkedin-conversions-api.mjs +6 -1
- package/dist/server/linkedin-conversions-api.mjs.map +1 -1
- package/dist/server/meta-capi.cjs +7 -1
- package/dist/server/meta-capi.cjs.map +1 -1
- package/dist/server/meta-capi.d.cts.map +1 -1
- package/dist/server/meta-capi.d.mts.map +1 -1
- package/dist/server/meta-capi.mjs +7 -1
- package/dist/server/meta-capi.mjs.map +1 -1
- package/dist/server/meta-conversions-api.cjs +3 -1
- 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 +3 -1
- package/dist/server/meta-conversions-api.mjs.map +1 -1
- package/dist/server/microsoft-conversions-api.cjs +3 -1
- package/dist/server/microsoft-conversions-api.cjs.map +1 -1
- package/dist/server/microsoft-conversions-api.d.cts.map +1 -1
- package/dist/server/microsoft-conversions-api.d.mts.map +1 -1
- package/dist/server/microsoft-conversions-api.mjs +3 -1
- package/dist/server/microsoft-conversions-api.mjs.map +1 -1
- package/dist/server/openai-conversions-api.cjs +13 -12
- 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 +14 -13
- package/dist/server/openai-conversions-api.mjs.map +1 -1
- package/dist/server/reddit-conversions-api.cjs +6 -2
- package/dist/server/reddit-conversions-api.cjs.map +1 -1
- package/dist/server/reddit-conversions-api.d.cts +10 -1
- package/dist/server/reddit-conversions-api.d.cts.map +1 -1
- package/dist/server/reddit-conversions-api.d.mts +10 -1
- package/dist/server/reddit-conversions-api.d.mts.map +1 -1
- package/dist/server/reddit-conversions-api.mjs +6 -2
- package/dist/server/reddit-conversions-api.mjs.map +1 -1
- package/dist/third-parties/openai-pixel.cjs +29 -13
- package/dist/third-parties/openai-pixel.cjs.map +1 -1
- package/dist/third-parties/openai-pixel.d.cts +4 -3
- package/dist/third-parties/openai-pixel.d.cts.map +1 -1
- package/dist/third-parties/openai-pixel.d.mts +4 -3
- package/dist/third-parties/openai-pixel.d.mts.map +1 -1
- package/dist/third-parties/openai-pixel.mjs +30 -14
- package/dist/third-parties/openai-pixel.mjs.map +1 -1
- package/dist/track/oaiq.cjs +57 -9
- package/dist/track/oaiq.cjs.map +1 -1
- package/dist/track/oaiq.d.cts +33 -7
- package/dist/track/oaiq.d.cts.map +1 -1
- package/dist/track/oaiq.d.mts +33 -7
- package/dist/track/oaiq.d.mts.map +1 -1
- package/dist/track/oaiq.mjs +55 -10
- package/dist/track/oaiq.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { IGNORED_EVENTS } from "../third-parties/ignored-events.mjs";
|
|
|
2
2
|
import { getFirst } from "../utils/field.mjs";
|
|
3
3
|
import { resolveActionSource } from "./action-source.mjs";
|
|
4
4
|
import { redditClickId, redditUuid } from "./click-ids.mjs";
|
|
5
|
+
import { pageLocation } from "./page-location.mjs";
|
|
5
6
|
import { mapRDTEvent, mapServerStandardEvent } from "../track/rdt.mjs";
|
|
6
7
|
import { fetch } from "@shware/utils";
|
|
7
8
|
//#region src/server/reddit-conversions-api.ts
|
|
@@ -12,7 +13,8 @@ function getServerEvent(event, data, actionSource) {
|
|
|
12
13
|
return {
|
|
13
14
|
click_id: redditClickId(tags),
|
|
14
15
|
event_at: new Date(created_at).getTime(),
|
|
15
|
-
action_source: source === "web" ? "WEBSITE" : source === "app" ? "APP" : "
|
|
16
|
+
action_source: source === "web" ? "WEBSITE" : source === "app" ? "APP" : "OTHER",
|
|
17
|
+
event_source_url: source === "web" ? pageLocation(tags) : void 0,
|
|
16
18
|
type: {
|
|
17
19
|
tracking_type: type === "Custom" ? "CUSTOM" : mapServerStandardEvent(type),
|
|
18
20
|
custom_event_name: type === "Custom" ? params.customEventName : void 0
|
|
@@ -40,10 +42,12 @@ function getServerEvent(event, data, actionSource) {
|
|
|
40
42
|
}
|
|
41
43
|
};
|
|
42
44
|
}
|
|
45
|
+
/** `event_at` "can't be older than seven days"; a minute's margin for the trip. */
|
|
46
|
+
const MAX_EVENT_AGE_MS = 60474e4;
|
|
43
47
|
async function sendEvents(accessToken, pixelId, events, data = {}, testId, actionSource) {
|
|
44
48
|
const dto = { data: {
|
|
45
49
|
test_id: testId,
|
|
46
|
-
events: events.filter((event) => !IGNORED_EVENTS.includes(event.name)).map((event) => getServerEvent(event, data, actionSource))
|
|
50
|
+
events: events.filter((event) => !IGNORED_EVENTS.includes(event.name)).filter((event) => Date.now() - Date.parse(event.created_at) <= MAX_EVENT_AGE_MS).map((event) => getServerEvent(event, data, actionSource))
|
|
47
51
|
} };
|
|
48
52
|
if (dto.data.events.length === 0) return;
|
|
49
53
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reddit-conversions-api.mjs","names":[],"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type ServerStandardEvent, mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { type EventActionSource, resolveActionSource } from './action-source';\nimport { redditClickId, redditUuid } from './click-ids';\n\n/**\n * https://ads-api.reddit.com/docs/v3/operations/Post%20Conversion%20Events\n * https://business.reddithelp.com/s/article/map-a-catalog-to-a-signal-source\n */\nexport interface RedditEvent {\n /** Match keys: Share user identifiers to match conversions to a Reddit ad engagement. */\n click_id?: string;\n\n /** Unix epoch timestamp in milliseconds, event_at can't be older than seven days. */\n event_at: number;\n\n action_source: 'WEBSITE' | 'APP' |
|
|
1
|
+
{"version":3,"file":"reddit-conversions-api.mjs","names":[],"sources":["../../src/server/reddit-conversions-api.ts"],"sourcesContent":["import { fetch } from '@shware/utils';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { type ServerStandardEvent, mapRDTEvent, mapServerStandardEvent } from '../track/rdt';\nimport type { TrackEvent, UserProvidedData } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { type EventActionSource, resolveActionSource } from './action-source';\nimport { redditClickId, redditUuid } from './click-ids';\nimport { pageLocation } from './page-location';\n\n/**\n * https://ads-api.reddit.com/docs/v3/operations/Post%20Conversion%20Events\n * https://business.reddithelp.com/s/article/map-a-catalog-to-a-signal-source\n */\nexport interface RedditEvent {\n /** Match keys: Share user identifiers to match conversions to a Reddit ad engagement. */\n click_id?: string;\n\n /** Unix epoch timestamp in milliseconds, event_at can't be older than seven days. */\n event_at: number;\n\n /**\n * Where the conversion happened. Pixel events are deduplicated only against `WEBSITE` ones.\n * https://business.reddithelp.com/s/article/Conversions-API#measure-impact-across-channels\n */\n action_source: 'WEBSITE' | 'APP' | 'PHYSICAL_STORE' | 'OTHER';\n\n /**\n * The page the event happened on, for `WEBSITE` events: Reddit reads the domain from it, and the\n * click id when `click_id` is missing.\n */\n event_source_url?: string;\n\n type: {\n tracking_type: ServerStandardEvent | 'CUSTOM';\n custom_event_name?: string;\n };\n\n /**\n * Event metadata\n * Share as much additional information about your conversion event as you'd like. If you're\n * using the Conversions API with the pixel, conversion_id is required for deduplication.\n */\n metadata?: {\n conversion_id?: string;\n currency?: string; // ISO 4217 3-letter currency code\n item_count?: number;\n value?: number;\n products?: { id: string; name?: string; category?: string }[];\n };\n\n user?: {\n email?: string;\n external_id?: string;\n ip_address?: string;\n phone_number?: string;\n user_agent?: string;\n\n /** The Identifier for Advertisers (IDFA) of the user's Apple device. */\n idfa?: string;\n\n /** The Android Advertising ID (AAID) of the user's Android device. */\n aaid?: string;\n /**\n * The value from the first-party Pixel _rdt_uuid cookie on your domain. Note that it is in\n * the {timestamp}.{uuid} format. You may use the full value or just the UUID portion.\n * Example: 1684189007728.7c73f2ae-a433-4d7b-9838-f467da98f48e\n */\n uuid?: string;\n\n screen_dimensions?: { width: number; height: number };\n\n /**\n * A structure of data processing options to specify the processing type for the event\n * https://business.reddithelp.com/s/article/Limited-Data-Use\n */\n data_processing_options?: {\n country: string;\n region: string;\n modes: string[] | ['LDU'];\n };\n };\n}\n\nexport interface CreateRedditEventDTO {\n data: { test_id?: string; events: RedditEvent[] };\n}\n\nexport function getServerEvent(\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n event: TrackEvent<any>,\n data: UserProvidedData,\n actionSource?: EventActionSource\n): RedditEvent {\n const { id, name, properties, tags, platform, created_at } = event;\n const [type, params] = mapRDTEvent(name, properties, id);\n // An offline conversion — in store, from a CRM, over the phone — is `OTHER` rather than\n // `PHYSICAL_STORE`, which only one of those is; so is an undeterminable platform.\n const source = resolveActionSource(platform, actionSource);\n\n return {\n click_id: redditClickId(tags),\n event_at: new Date(created_at).getTime(),\n action_source: source === 'web' ? 'WEBSITE' : source === 'app' ? 'APP' : 'OTHER',\n event_source_url: source === 'web' ? pageLocation(tags) : undefined,\n type: {\n tracking_type: type === 'Custom' ? 'CUSTOM' : mapServerStandardEvent(type),\n custom_event_name: type === 'Custom' ? params.customEventName : undefined,\n },\n metadata: {\n conversion_id: id,\n currency:\n 'currency' in params && typeof params.currency === 'string'\n ? params.currency.toUpperCase()\n : undefined,\n item_count:\n 'itemCount' in params && typeof params.itemCount === 'number'\n ? params.itemCount\n : undefined,\n value: 'value' in params && typeof params.value === 'number' ? params.value : undefined,\n products:\n 'products' in params && Array.isArray(params.products) && params.products.length > 0\n ? params.products\n : undefined,\n },\n user: {\n email: getFirst(data.email),\n external_id: data.user_id,\n ip_address: data.ip_address,\n phone_number: getFirst(data.phone_number),\n user_agent: data.user_agent,\n idfa: platform === 'ios' ? tags.advertising_id : undefined,\n aaid: platform === 'android' ? tags.advertising_id : undefined,\n uuid: redditUuid(tags),\n screen_dimensions:\n tags.screen_width && tags.screen_height\n ? { width: tags.screen_width, height: tags.screen_height }\n : undefined,\n },\n };\n}\n\n/** `event_at` \"can't be older than seven days\"; a minute's margin for the trip. */\nconst MAX_EVENT_AGE_MS = 7 * 24 * 60 * 60 * 1000 - 60 * 1000;\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 testId?: string,\n actionSource?: EventActionSource\n) {\n const dto: CreateRedditEventDTO = {\n data: {\n test_id: testId,\n events: events\n .filter((event) => !IGNORED_EVENTS.includes(event.name))\n .filter((event) => Date.now() - Date.parse(event.created_at) <= MAX_EVENT_AGE_MS)\n .map((event) => getServerEvent(event, data, actionSource)),\n },\n };\n\n if (dto.data.events.length === 0) return;\n\n try {\n const response = await fetch(\n `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify(dto),\n }\n );\n if (response.ok) return;\n const { status } = response;\n const message = await response.text();\n console.error(`Failed to send Reddit conversion, status: ${status}, body: ${message}`);\n } catch (error) {\n console.error('Failed to send Reddit conversion, network error:', error);\n }\n}\n"],"mappings":";;;;;;;;AAuFA,SAAgB,eAEd,OACA,MACA,cACa;CACb,MAAM,EAAE,IAAI,MAAM,YAAY,MAAM,UAAU,eAAe;CAC7D,MAAM,CAAC,MAAM,UAAU,YAAY,MAAM,YAAY,EAAE;CAGvD,MAAM,SAAS,oBAAoB,UAAU,YAAY;CAEzD,OAAO;EACL,UAAU,cAAc,IAAI;EAC5B,UAAU,IAAI,KAAK,UAAU,CAAC,CAAC,QAAQ;EACvC,eAAe,WAAW,QAAQ,YAAY,WAAW,QAAQ,QAAQ;EACzE,kBAAkB,WAAW,QAAQ,aAAa,IAAI,IAAI,KAAA;EAC1D,MAAM;GACJ,eAAe,SAAS,WAAW,WAAW,uBAAuB,IAAI;GACzE,mBAAmB,SAAS,WAAW,OAAO,kBAAkB,KAAA;EAClE;EACA,UAAU;GACR,eAAe;GACf,UACE,cAAc,UAAU,OAAO,OAAO,aAAa,WAC/C,OAAO,SAAS,YAAY,IAC5B,KAAA;GACN,YACE,eAAe,UAAU,OAAO,OAAO,cAAc,WACjD,OAAO,YACP,KAAA;GACN,OAAO,WAAW,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ,KAAA;GAC9E,UACE,cAAc,UAAU,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,SAAS,IAC/E,OAAO,WACP,KAAA;EACR;EACA,MAAM;GACJ,OAAO,SAAS,KAAK,KAAK;GAC1B,aAAa,KAAK;GAClB,YAAY,KAAK;GACjB,cAAc,SAAS,KAAK,YAAY;GACxC,YAAY,KAAK;GACjB,MAAM,aAAa,QAAQ,KAAK,iBAAiB,KAAA;GACjD,MAAM,aAAa,YAAY,KAAK,iBAAiB,KAAA;GACrD,MAAM,WAAW,IAAI;GACrB,mBACE,KAAK,gBAAgB,KAAK,gBACtB;IAAE,OAAO,KAAK;IAAc,QAAQ,KAAK;GAAc,IACvD,KAAA;EACR;CACF;AACF;;AAGA,MAAM,mBAAmB;AAEzB,eAAsB,WACpB,aACA,SAEA,QACA,OAAyB,CAAC,GAC1B,QACA,cACA;CACA,MAAM,MAA4B,EAChC,MAAM;EACJ,SAAS;EACT,QAAQ,OACL,QAAQ,UAAU,CAAC,eAAe,SAAS,MAAM,IAAI,CAAC,CAAC,CACvD,QAAQ,UAAU,KAAK,IAAI,IAAI,KAAK,MAAM,MAAM,UAAU,KAAK,gBAAgB,CAAC,CAChF,KAAK,UAAU,eAAe,OAAO,MAAM,YAAY,CAAC;CAC7D,EACF;CAEA,IAAI,IAAI,KAAK,OAAO,WAAW,GAAG;CAElC,IAAI;EACF,MAAM,WAAW,MAAM,MACrB,4CAA4C,QAAQ,qBACpD;GACE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,QAAQ;IACR,eAAe,UAAU;GAC3B;GACA,MAAM,KAAK,UAAU,GAAG;EAC1B,CACF;EACA,IAAI,SAAS,IAAI;EACjB,MAAM,EAAE,WAAW;EACnB,MAAM,UAAU,MAAM,SAAS,KAAK;EACpC,QAAQ,MAAM,6CAA6C,OAAO,UAAU,SAAS;CACvF,SAAS,OAAO;EACd,QAAQ,MAAM,oDAAoD,KAAK;CACzE;AACF"}
|
|
@@ -21,16 +21,20 @@ function sendOpenAIEvent(name, properties, eventId) {
|
|
|
21
21
|
if (window.location.host.includes("127.0.0.1")) return;
|
|
22
22
|
if (window.location.host.includes("localhost")) return;
|
|
23
23
|
const { type, data } = require_track_oaiq.mapOAIEvent(name, properties);
|
|
24
|
-
if (type === "custom")
|
|
25
|
-
|
|
26
|
-
custom_event_name
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
if (type === "custom") {
|
|
25
|
+
const custom_event_name = require_track_oaiq.oaiCustomEventName(name);
|
|
26
|
+
if (!custom_event_name) return;
|
|
27
|
+
window.oaiq("measure", "custom", clean(data), {
|
|
28
|
+
event_id: eventId,
|
|
29
|
+
custom_event_name
|
|
30
|
+
});
|
|
31
|
+
} else window.oaiq("measure", type, clean(data), { event_id: eventId });
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
31
|
-
* Re-initialize the pixel with hashed user identity for better conversion matching. Email
|
|
32
|
-
* external id are hashed client-side; geographic
|
|
33
|
-
* so the `init` call is deferred until the digests
|
|
34
|
+
* Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,
|
|
35
|
+
* external id and names are normalized as OpenAI documents and hashed client-side; geographic
|
|
36
|
+
* fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests
|
|
37
|
+
* resolve.
|
|
34
38
|
*/
|
|
35
39
|
function setOpenAIUser(pixelId) {
|
|
36
40
|
return ({ user_id, user_data }) => {
|
|
@@ -39,12 +43,14 @@ function setOpenAIUser(pixelId) {
|
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
const oaiq = window.oaiq;
|
|
42
|
-
const email = require_utils_field.getFirst(user_data?.email)?.trim().toLowerCase();
|
|
46
|
+
const email = require_utils_field.getFirst(user_data?.email)?.trim().toLowerCase() || void 0;
|
|
47
|
+
const phone = require_utils_field.getFirst(user_data?.phone_number);
|
|
43
48
|
const address = require_utils_field.getFirst(user_data?.address);
|
|
44
49
|
const base = {
|
|
45
50
|
country: address?.country?.trim().toUpperCase(),
|
|
46
|
-
city: address?.city?.trim()
|
|
47
|
-
|
|
51
|
+
city: address?.city?.trim(),
|
|
52
|
+
region: address?.region?.trim(),
|
|
53
|
+
postal_code: address?.postal_code?.trim()
|
|
48
54
|
};
|
|
49
55
|
const init = (hashed) => {
|
|
50
56
|
oaiq("init", {
|
|
@@ -55,9 +61,19 @@ function setOpenAIUser(pixelId) {
|
|
|
55
61
|
})
|
|
56
62
|
});
|
|
57
63
|
};
|
|
58
|
-
|
|
64
|
+
const hash = (value) => value ? require_utils_sha256.sha256(value) : void 0;
|
|
65
|
+
Promise.all([
|
|
66
|
+
hash(email),
|
|
67
|
+
hash(phone ? require_track_oaiq.normalizeOAIPhone(phone) : void 0),
|
|
68
|
+
hash(user_id?.trim()),
|
|
69
|
+
hash(require_track_oaiq.normalizeOAIName(address?.first_name)),
|
|
70
|
+
hash(require_track_oaiq.normalizeOAIName(address?.last_name))
|
|
71
|
+
]).then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) => init({
|
|
59
72
|
email_sha256,
|
|
60
|
-
|
|
73
|
+
phone_number_sha256,
|
|
74
|
+
external_id_sha256,
|
|
75
|
+
first_name_sha256: first,
|
|
76
|
+
last_name_sha256: last
|
|
61
77
|
})).catch(() => init({}));
|
|
62
78
|
};
|
|
63
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-pixel.cjs","names":["NON_AD_EVENTS","mapOAIEvent","getFirst","sha256"],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {
|
|
1
|
+
{"version":3,"file":"openai-pixel.cjs","names":["NON_AD_EVENTS","mapOAIEvent","oaiCustomEventName","getFirst","sha256","normalizeOAIPhone","normalizeOAIName"],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {\n NON_AD_EVENTS,\n type OAIQ,\n type OAIQUser,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /** Undefined until the OpenAI pixel script has loaded. */\n oaiq?: OAIQ['oaiq'];\n }\n}\n\n/** Drop `undefined` fields so the SDK only receives populated values. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Forward an internal track event to the OpenAI measurement pixel.\n * `eventId` is reused as the OpenAI `event_id` so browser events deduplicate against the\n * Conversions API. https://developers.openai.com/ads/measurement-pixel\n */\nexport function sendOpenAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n if (NON_AD_EVENTS.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const { type, data } = mapOAIEvent(name, properties);\n if (type === 'custom') {\n // Named as the Conversions API names it, so the two deduplicate; skipped when it cannot be.\n const custom_event_name = oaiCustomEventName(name);\n if (!custom_event_name) return;\n window.oaiq('measure', 'custom', clean(data), { event_id: eventId, custom_event_name });\n } else {\n window.oaiq('measure', type, clean(data), { event_id: eventId });\n }\n}\n\n/**\n * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,\n * external id and names are normalized as OpenAI documents and hashed client-side; geographic\n * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests\n * resolve.\n */\nexport function setOpenAIUser(pixelId: string) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n // Capture the narrowed reference so the deferred `init` closure keeps it non-optional.\n const oaiq = window.oaiq;\n\n const email = getFirst(user_data?.email)?.trim().toLowerCase() || undefined;\n const phone = getFirst(user_data?.phone_number);\n const address = getFirst(user_data?.address);\n\n const base: OAIQUser = {\n country: address?.country?.trim().toUpperCase(),\n city: address?.city?.trim(),\n region: address?.region?.trim(),\n postal_code: address?.postal_code?.trim(),\n };\n\n const init = (hashed: Partial<OAIQUser>) => {\n oaiq('init', { pixelId, user: clean({ ...base, ...hashed }) });\n };\n\n const hash = (value: string | undefined) => (value ? sha256(value) : undefined);\n Promise.all([\n hash(email),\n hash(phone ? normalizeOAIPhone(phone) : undefined),\n hash(user_id?.trim()),\n hash(normalizeOAIName(address?.first_name)),\n hash(normalizeOAIName(address?.last_name)),\n ])\n .then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) =>\n init({\n email_sha256,\n phone_number_sha256,\n external_id_sha256,\n first_name_sha256: first,\n last_name_sha256: last,\n })\n )\n .catch(() => init({}));\n };\n}\n"],"mappings":";;;;;;AAuBA,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;AAOA,SAAgB,gBACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAIA,mBAAAA,cAAc,SAAS,IAAI,GAAG;CAClC,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,EAAE,MAAM,SAASC,mBAAAA,YAAY,MAAM,UAAU;CACnD,IAAI,SAAS,UAAU;EAErB,MAAM,oBAAoBC,mBAAAA,mBAAmB,IAAI;EACjD,IAAI,CAAC,mBAAmB;EACxB,OAAO,KAAK,WAAW,UAAU,MAAM,IAAI,GAAG;GAAE,UAAU;GAAS;EAAkB,CAAC;CACxF,OACE,OAAO,KAAK,WAAW,MAAM,MAAM,IAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAEnE;;;;;;;AAQA,SAAgB,cAAc,SAAiB;CAC7C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQC,oBAAAA,SAAS,WAAW,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY,KAAK,KAAA;EAClE,MAAM,QAAQA,oBAAAA,SAAS,WAAW,YAAY;EAC9C,MAAM,UAAUA,oBAAAA,SAAS,WAAW,OAAO;EAE3C,MAAM,OAAiB;GACrB,SAAS,SAAS,SAAS,KAAK,CAAC,CAAC,YAAY;GAC9C,MAAM,SAAS,MAAM,KAAK;GAC1B,QAAQ,SAAS,QAAQ,KAAK;GAC9B,aAAa,SAAS,aAAa,KAAK;EAC1C;EAEA,MAAM,QAAQ,WAA8B;GAC1C,KAAK,QAAQ;IAAE;IAAS,MAAM,MAAM;KAAE,GAAG;KAAM,GAAG;IAAO,CAAC;GAAE,CAAC;EAC/D;EAEA,MAAM,QAAQ,UAA+B,QAAQC,qBAAAA,OAAO,KAAK,IAAI,KAAA;EACrE,QAAQ,IAAI;GACV,KAAK,KAAK;GACV,KAAK,QAAQC,mBAAAA,kBAAkB,KAAK,IAAI,KAAA,CAAS;GACjD,KAAK,SAAS,KAAK,CAAC;GACpB,KAAKC,mBAAAA,iBAAiB,SAAS,UAAU,CAAC;GAC1C,KAAKA,mBAAAA,iBAAiB,SAAS,SAAS,CAAC;EAC3C,CAAC,CAAC,CACC,MAAM,CAAC,cAAc,qBAAqB,oBAAoB,OAAO,UACpE,KAAK;GACH;GACA;GACA;GACA,mBAAmB;GACnB,kBAAkB;EACpB,CAAC,CACH,CAAC,CACA,YAAY,KAAK,CAAC,CAAC,CAAC;CACzB;AACF"}
|
|
@@ -15,9 +15,10 @@ declare global {
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function sendOpenAIEvent<T extends EventName>(name: TrackName<T>, properties?: TrackProperties<T>, eventId?: string): void;
|
|
17
17
|
/**
|
|
18
|
-
* Re-initialize the pixel with hashed user identity for better conversion matching. Email
|
|
19
|
-
* external id are hashed client-side; geographic
|
|
20
|
-
* so the `init` call is deferred until the digests
|
|
18
|
+
* Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,
|
|
19
|
+
* external id and names are normalized as OpenAI documents and hashed client-side; geographic
|
|
20
|
+
* fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests
|
|
21
|
+
* resolve.
|
|
21
22
|
*/
|
|
22
23
|
export declare function setOpenAIUser(pixelId: string): ({ user_id, user_data }: UpdateVisitorDTO) => void;
|
|
23
24
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-pixel.d.cts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"openai-pixel.d.cts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;QAcQ;YACI;;IAER,OAAO;;;;;;;;wBAeK,gBAAgB,UAAU,WACxC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;wBA2Bc,cAAc,qBAAe,SAAA,aACX"}
|
|
@@ -15,9 +15,10 @@ declare global {
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function sendOpenAIEvent<T extends EventName>(name: TrackName<T>, properties?: TrackProperties<T>, eventId?: string): void;
|
|
17
17
|
/**
|
|
18
|
-
* Re-initialize the pixel with hashed user identity for better conversion matching. Email
|
|
19
|
-
* external id are hashed client-side; geographic
|
|
20
|
-
* so the `init` call is deferred until the digests
|
|
18
|
+
* Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,
|
|
19
|
+
* external id and names are normalized as OpenAI documents and hashed client-side; geographic
|
|
20
|
+
* fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests
|
|
21
|
+
* resolve.
|
|
21
22
|
*/
|
|
22
23
|
export declare function setOpenAIUser(pixelId: string): ({ user_id, user_data }: UpdateVisitorDTO) => void;
|
|
23
24
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-pixel.d.mts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"openai-pixel.d.mts","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"mappings":";;;;QAcQ;YACI;;IAER,OAAO;;;;;;;;wBAeK,gBAAgB,UAAU,WACxC,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B;;;;;;;wBA2Bc,cAAc,qBAAe,SAAA,aACX"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getFirst } from "../utils/field.mjs";
|
|
2
2
|
import { sha256 } from "../utils/sha256.mjs";
|
|
3
|
-
import { NON_AD_EVENTS, mapOAIEvent } from "../track/oaiq.mjs";
|
|
3
|
+
import { NON_AD_EVENTS, mapOAIEvent, normalizeOAIName, normalizeOAIPhone, oaiCustomEventName } from "../track/oaiq.mjs";
|
|
4
4
|
//#region src/third-parties/openai-pixel.ts
|
|
5
5
|
/** Drop `undefined` fields so the SDK only receives populated values. */
|
|
6
6
|
function clean(value) {
|
|
@@ -20,16 +20,20 @@ function sendOpenAIEvent(name, properties, eventId) {
|
|
|
20
20
|
if (window.location.host.includes("127.0.0.1")) return;
|
|
21
21
|
if (window.location.host.includes("localhost")) return;
|
|
22
22
|
const { type, data } = mapOAIEvent(name, properties);
|
|
23
|
-
if (type === "custom")
|
|
24
|
-
|
|
25
|
-
custom_event_name
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
if (type === "custom") {
|
|
24
|
+
const custom_event_name = oaiCustomEventName(name);
|
|
25
|
+
if (!custom_event_name) return;
|
|
26
|
+
window.oaiq("measure", "custom", clean(data), {
|
|
27
|
+
event_id: eventId,
|
|
28
|
+
custom_event_name
|
|
29
|
+
});
|
|
30
|
+
} else window.oaiq("measure", type, clean(data), { event_id: eventId });
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
|
-
* Re-initialize the pixel with hashed user identity for better conversion matching. Email
|
|
31
|
-
* external id are hashed client-side; geographic
|
|
32
|
-
* so the `init` call is deferred until the digests
|
|
33
|
+
* Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,
|
|
34
|
+
* external id and names are normalized as OpenAI documents and hashed client-side; geographic
|
|
35
|
+
* fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests
|
|
36
|
+
* resolve.
|
|
33
37
|
*/
|
|
34
38
|
function setOpenAIUser(pixelId) {
|
|
35
39
|
return ({ user_id, user_data }) => {
|
|
@@ -38,12 +42,14 @@ function setOpenAIUser(pixelId) {
|
|
|
38
42
|
return;
|
|
39
43
|
}
|
|
40
44
|
const oaiq = window.oaiq;
|
|
41
|
-
const email = getFirst(user_data?.email)?.trim().toLowerCase();
|
|
45
|
+
const email = getFirst(user_data?.email)?.trim().toLowerCase() || void 0;
|
|
46
|
+
const phone = getFirst(user_data?.phone_number);
|
|
42
47
|
const address = getFirst(user_data?.address);
|
|
43
48
|
const base = {
|
|
44
49
|
country: address?.country?.trim().toUpperCase(),
|
|
45
|
-
city: address?.city?.trim()
|
|
46
|
-
|
|
50
|
+
city: address?.city?.trim(),
|
|
51
|
+
region: address?.region?.trim(),
|
|
52
|
+
postal_code: address?.postal_code?.trim()
|
|
47
53
|
};
|
|
48
54
|
const init = (hashed) => {
|
|
49
55
|
oaiq("init", {
|
|
@@ -54,9 +60,19 @@ function setOpenAIUser(pixelId) {
|
|
|
54
60
|
})
|
|
55
61
|
});
|
|
56
62
|
};
|
|
57
|
-
|
|
63
|
+
const hash = (value) => value ? sha256(value) : void 0;
|
|
64
|
+
Promise.all([
|
|
65
|
+
hash(email),
|
|
66
|
+
hash(phone ? normalizeOAIPhone(phone) : void 0),
|
|
67
|
+
hash(user_id?.trim()),
|
|
68
|
+
hash(normalizeOAIName(address?.first_name)),
|
|
69
|
+
hash(normalizeOAIName(address?.last_name))
|
|
70
|
+
]).then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) => init({
|
|
58
71
|
email_sha256,
|
|
59
|
-
|
|
72
|
+
phone_number_sha256,
|
|
73
|
+
external_id_sha256,
|
|
74
|
+
first_name_sha256: first,
|
|
75
|
+
last_name_sha256: last
|
|
60
76
|
})).catch(() => init({}));
|
|
61
77
|
};
|
|
62
78
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-pixel.mjs","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {
|
|
1
|
+
{"version":3,"file":"openai-pixel.mjs","names":[],"sources":["../../src/third-parties/openai-pixel.ts"],"sourcesContent":["import type { UpdateVisitorDTO } from '../schema/index';\nimport {\n NON_AD_EVENTS,\n type OAIQ,\n type OAIQUser,\n mapOAIEvent,\n normalizeOAIName,\n normalizeOAIPhone,\n oaiCustomEventName,\n} from '../track/oaiq';\nimport type { EventName, TrackName, TrackProperties } from '../track/types';\nimport { getFirst } from '../utils/field';\nimport { sha256 } from '../utils/sha256';\n\ndeclare global {\n interface Window {\n /** Undefined until the OpenAI pixel script has loaded. */\n oaiq?: OAIQ['oaiq'];\n }\n}\n\n/** Drop `undefined` fields so the SDK only receives populated values. */\n// oxlint-disable-next-line @typescript-eslint/no-explicit-any\nfunction clean(value: unknown): any {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Forward an internal track event to the OpenAI measurement pixel.\n * `eventId` is reused as the OpenAI `event_id` so browser events deduplicate against the\n * Conversions API. https://developers.openai.com/ads/measurement-pixel\n */\nexport function sendOpenAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n eventId?: string\n) {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n if (NON_AD_EVENTS.includes(name)) return;\n if (window.location.host.includes('127.0.0.1')) return;\n if (window.location.host.includes('localhost')) return;\n\n const { type, data } = mapOAIEvent(name, properties);\n if (type === 'custom') {\n // Named as the Conversions API names it, so the two deduplicate; skipped when it cannot be.\n const custom_event_name = oaiCustomEventName(name);\n if (!custom_event_name) return;\n window.oaiq('measure', 'custom', clean(data), { event_id: eventId, custom_event_name });\n } else {\n window.oaiq('measure', type, clean(data), { event_id: eventId });\n }\n}\n\n/**\n * Re-initialize the pixel with hashed user identity for better conversion matching. Email, phone,\n * external id and names are normalized as OpenAI documents and hashed client-side; geographic\n * fields are sent raw. Hashing is asynchronous, so the `init` call is deferred until the digests\n * resolve.\n */\nexport function setOpenAIUser(pixelId: string) {\n return ({ user_id, user_data }: UpdateVisitorDTO) => {\n if (typeof window === 'undefined' || !window.oaiq) {\n console.warn('oaiq has not been initialized');\n return;\n }\n // Capture the narrowed reference so the deferred `init` closure keeps it non-optional.\n const oaiq = window.oaiq;\n\n const email = getFirst(user_data?.email)?.trim().toLowerCase() || undefined;\n const phone = getFirst(user_data?.phone_number);\n const address = getFirst(user_data?.address);\n\n const base: OAIQUser = {\n country: address?.country?.trim().toUpperCase(),\n city: address?.city?.trim(),\n region: address?.region?.trim(),\n postal_code: address?.postal_code?.trim(),\n };\n\n const init = (hashed: Partial<OAIQUser>) => {\n oaiq('init', { pixelId, user: clean({ ...base, ...hashed }) });\n };\n\n const hash = (value: string | undefined) => (value ? sha256(value) : undefined);\n Promise.all([\n hash(email),\n hash(phone ? normalizeOAIPhone(phone) : undefined),\n hash(user_id?.trim()),\n hash(normalizeOAIName(address?.first_name)),\n hash(normalizeOAIName(address?.last_name)),\n ])\n .then(([email_sha256, phone_number_sha256, external_id_sha256, first, last]) =>\n init({\n email_sha256,\n phone_number_sha256,\n external_id_sha256,\n first_name_sha256: first,\n last_name_sha256: last,\n })\n )\n .catch(() => init({}));\n };\n}\n"],"mappings":";;;;;AAuBA,SAAS,MAAM,OAAqB;CAClC,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;AAOA,SAAgB,gBACd,MACA,YACA,SACA;CACA,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;EACjD,QAAQ,KAAK,+BAA+B;EAC5C;CACF;CACA,IAAI,cAAc,SAAS,IAAI,GAAG;CAClC,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAChD,IAAI,OAAO,SAAS,KAAK,SAAS,WAAW,GAAG;CAEhD,MAAM,EAAE,MAAM,SAAS,YAAY,MAAM,UAAU;CACnD,IAAI,SAAS,UAAU;EAErB,MAAM,oBAAoB,mBAAmB,IAAI;EACjD,IAAI,CAAC,mBAAmB;EACxB,OAAO,KAAK,WAAW,UAAU,MAAM,IAAI,GAAG;GAAE,UAAU;GAAS;EAAkB,CAAC;CACxF,OACE,OAAO,KAAK,WAAW,MAAM,MAAM,IAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAEnE;;;;;;;AAQA,SAAgB,cAAc,SAAiB;CAC7C,QAAQ,EAAE,SAAS,gBAAkC;EACnD,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,MAAM;GACjD,QAAQ,KAAK,+BAA+B;GAC5C;EACF;EAEA,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQ,SAAS,WAAW,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,YAAY,KAAK,KAAA;EAClE,MAAM,QAAQ,SAAS,WAAW,YAAY;EAC9C,MAAM,UAAU,SAAS,WAAW,OAAO;EAE3C,MAAM,OAAiB;GACrB,SAAS,SAAS,SAAS,KAAK,CAAC,CAAC,YAAY;GAC9C,MAAM,SAAS,MAAM,KAAK;GAC1B,QAAQ,SAAS,QAAQ,KAAK;GAC9B,aAAa,SAAS,aAAa,KAAK;EAC1C;EAEA,MAAM,QAAQ,WAA8B;GAC1C,KAAK,QAAQ;IAAE;IAAS,MAAM,MAAM;KAAE,GAAG;KAAM,GAAG;IAAO,CAAC;GAAE,CAAC;EAC/D;EAEA,MAAM,QAAQ,UAA+B,QAAQ,OAAO,KAAK,IAAI,KAAA;EACrE,QAAQ,IAAI;GACV,KAAK,KAAK;GACV,KAAK,QAAQ,kBAAkB,KAAK,IAAI,KAAA,CAAS;GACjD,KAAK,SAAS,KAAK,CAAC;GACpB,KAAK,iBAAiB,SAAS,UAAU,CAAC;GAC1C,KAAK,iBAAiB,SAAS,SAAS,CAAC;EAC3C,CAAC,CAAC,CACC,MAAM,CAAC,cAAc,qBAAqB,oBAAoB,OAAO,UACpE,KAAK;GACH;GACA;GACA;GACA,mBAAmB;GACnB,kBAAkB;EACpB,CAAC,CACH,CAAC,CACA,YAAY,KAAK,CAAC,CAAC,CAAC;CACzB;AACF"}
|
package/dist/track/oaiq.cjs
CHANGED
|
@@ -10,6 +10,43 @@ const NON_AD_EVENTS = [
|
|
|
10
10
|
"view_promotion",
|
|
11
11
|
"select_promotion"
|
|
12
12
|
];
|
|
13
|
+
const STANDARD_EVENTS = [
|
|
14
|
+
"appointment_scheduled",
|
|
15
|
+
"checkout_started",
|
|
16
|
+
"contents_viewed",
|
|
17
|
+
"items_added",
|
|
18
|
+
"lead_created",
|
|
19
|
+
"order_created",
|
|
20
|
+
"page_viewed",
|
|
21
|
+
"registration_completed",
|
|
22
|
+
"subscription_created",
|
|
23
|
+
"trial_started"
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
|
|
27
|
+
* starting and ending with a letter or digit, not a standard event name; the API lowercases it.
|
|
28
|
+
* Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when
|
|
29
|
+
* nothing usable is left — the event is then not sent at all. The pixel and the server both name
|
|
30
|
+
* a custom event through this, so the two still deduplicate.
|
|
31
|
+
* https://developers.openai.com/ads/supported-events
|
|
32
|
+
*/
|
|
33
|
+
function oaiCustomEventName(name) {
|
|
34
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").slice(0, 64).replace(/^[_-]+|[_-]+$/g, "");
|
|
35
|
+
if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return void 0;
|
|
36
|
+
return sanitized;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then
|
|
40
|
+
* the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.
|
|
41
|
+
*/
|
|
42
|
+
function normalizeOAIPhone(phone) {
|
|
43
|
+
const digits = phone.replace(/[\s().-]/g, "").replace(/^\+/, "").replace(/^0+/, "");
|
|
44
|
+
return /^\d{8,15}$/.test(digits) ? digits : void 0;
|
|
45
|
+
}
|
|
46
|
+
/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */
|
|
47
|
+
function normalizeOAIName(name) {
|
|
48
|
+
return name?.toLowerCase().replace(/[\s!-/:-@[-`{-~]/g, "") || void 0;
|
|
49
|
+
}
|
|
13
50
|
/**
|
|
14
51
|
* Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.
|
|
15
52
|
* Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),
|
|
@@ -44,24 +81,32 @@ const THREE_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
|
|
|
44
81
|
"OMR",
|
|
45
82
|
"TND"
|
|
46
83
|
]);
|
|
47
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None
|
|
86
|
+
* without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every
|
|
87
|
+
* `amount` — an amount alone would be invalid, not just imprecise.
|
|
88
|
+
*/
|
|
48
89
|
function toMinorUnits(value, currency) {
|
|
49
90
|
if (value === void 0 || value === null || Number.isNaN(value)) return void 0;
|
|
50
91
|
const code = currency?.toUpperCase();
|
|
92
|
+
if (!code) return void 0;
|
|
51
93
|
const exponent = code && ZERO_DECIMAL_CURRENCIES.has(code) ? 0 : code && THREE_DECIMAL_CURRENCIES.has(code) ? 3 : 2;
|
|
52
94
|
return Math.round(value * 10 ** exponent);
|
|
53
95
|
}
|
|
54
96
|
function mapContents(items, currency) {
|
|
55
97
|
if (!items || items.length === 0) return void 0;
|
|
56
98
|
const code = currency?.toUpperCase();
|
|
57
|
-
return items.map((item) =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
99
|
+
return items.map((item) => {
|
|
100
|
+
const amount = toMinorUnits(item.price, currency);
|
|
101
|
+
return {
|
|
102
|
+
id: item.item_id,
|
|
103
|
+
name: item.item_name,
|
|
104
|
+
content_type: item.item_category,
|
|
105
|
+
quantity: item.quantity,
|
|
106
|
+
amount,
|
|
107
|
+
currency: amount !== void 0 ? code : void 0
|
|
108
|
+
};
|
|
109
|
+
});
|
|
65
110
|
}
|
|
66
111
|
/** Safely read a property off loosely typed track properties without widening to `any`. */
|
|
67
112
|
function prop(properties, key) {
|
|
@@ -171,6 +216,9 @@ function mapOAIEvent(name, properties) {
|
|
|
171
216
|
exports.NON_AD_EVENTS = NON_AD_EVENTS;
|
|
172
217
|
exports.mapContents = mapContents;
|
|
173
218
|
exports.mapOAIEvent = mapOAIEvent;
|
|
219
|
+
exports.normalizeOAIName = normalizeOAIName;
|
|
220
|
+
exports.normalizeOAIPhone = normalizeOAIPhone;
|
|
221
|
+
exports.oaiCustomEventName = oaiCustomEventName;
|
|
174
222
|
exports.toMinorUnits = toMinorUnits;
|
|
175
223
|
|
|
176
224
|
//# sourceMappingURL=oaiq.cjs.map
|
package/dist/track/oaiq.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oaiq.cjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Email and external id\n * must be pre-hashed as lowercase 64-char SHA-256 hex strings; the rest are sent as raw values.\n * https://developers.openai.com/ads/measurement-pixel\n */\nexport type OAIQUser = {\n email_sha256?: string;\n external_id_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Lowercased, max 128 characters. */\n city?: string;\n /** Max 32 characters. */\n zip_code?: string;\n};\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/** Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => ({\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount: toMinorUnits(item.price, currency),\n currency: item.price !== undefined ? code : undefined,\n }));\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oaiq.cjs","names":[],"sources":["../../src/track/oaiq.ts"],"sourcesContent":["import type { Item } from './gtag';\nimport type { EventName, TrackName, TrackProperties } from './types';\n\nexport const NON_AD_EVENTS = [\n // metrics\n 'CLS',\n 'FCP',\n 'FID',\n 'INP',\n 'LCP',\n 'TTFB',\n // promotions\n 'view_promotion',\n 'select_promotion',\n];\n\n/**\n * Content item shared by `contents` and `plan_enrollment` events.\n * https://developers.openai.com/ads/supported-events\n */\nexport type ContentItem = {\n id?: string;\n name?: string;\n content_type?: string;\n quantity?: number;\n /** Per-item monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n};\n\nexport type ContentsData = {\n type: 'contents';\n /** Monetary value in ISO 4217 minor units (e.g., 12,999 = $129.99 USD). */\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomerActionData = {\n type: 'customer_action';\n amount?: number;\n currency?: string;\n};\n\nexport type PlanEnrollmentData = {\n type: 'plan_enrollment';\n plan_id?: string;\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type CustomEventData = {\n type: 'custom';\n amount?: number;\n currency?: string;\n contents?: ContentItem[];\n};\n\nexport type EventData = ContentsData | CustomerActionData | PlanEnrollmentData | CustomEventData;\n\n/**\n * Standard event names and the `data` shape each one expects.\n * https://developers.openai.com/ads/supported-events\n */\nexport type StandardEvents = {\n appointment_scheduled: CustomerActionData;\n checkout_started: ContentsData;\n contents_viewed: ContentsData;\n items_added: ContentsData;\n lead_created: CustomerActionData;\n order_created: ContentsData;\n page_viewed: ContentsData;\n registration_completed: CustomerActionData;\n subscription_created: PlanEnrollmentData;\n trial_started: PlanEnrollmentData;\n};\n\nexport type StandardEvent = keyof StandardEvents;\n\nconst STANDARD_EVENTS: readonly string[] = [\n 'appointment_scheduled',\n 'checkout_started',\n 'contents_viewed',\n 'items_added',\n 'lead_created',\n 'order_created',\n 'page_viewed',\n 'registration_completed',\n 'subscription_created',\n 'trial_started',\n] satisfies StandardEvent[];\n\n/**\n * A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,\n * starting and ending with a letter or digit, not a standard event name; the API lowercases it.\n * Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when\n * nothing usable is left — the event is then not sent at all. The pixel and the server both name\n * a custom event through this, so the two still deduplicate.\n * https://developers.openai.com/ads/supported-events\n */\nexport function oaiCustomEventName(name: string): string | undefined {\n const sanitized = name\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, '_')\n .slice(0, 64)\n .replace(/^[_-]+|[_-]+$/g, '');\n if (!sanitized || STANDARD_EVENTS.includes(sanitized)) return undefined;\n return sanitized;\n}\n\n/**\n * Identity fields passed to `oaiq(\"init\", ...)` for conversion matching. Identifiers are\n * normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.\n * https://developers.openai.com/ads/measurement-pixel#send-user-data\n */\nexport type OAIQUser = {\n email_sha256?: string;\n /** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */\n phone_number_sha256?: string;\n external_id_sha256?: string;\n /** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */\n first_name_sha256?: string;\n last_name_sha256?: string;\n /** Two-letter ISO 3166-1 country code (e.g. \"US\"). */\n country?: string;\n /** Max 128 characters; OpenAI trims and lowercases it. */\n city?: string;\n /** State, province or region, max 128 characters. */\n region?: string;\n postal_code?: string;\n};\n\n/**\n * A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then\n * the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.\n */\nexport function normalizeOAIPhone(phone: string): string | undefined {\n const digits = phone\n .replace(/[\\s().-]/g, '')\n .replace(/^\\+/, '')\n .replace(/^0+/, '');\n return /^\\d{8,15}$/.test(digits) ? digits : undefined;\n}\n\n/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */\nexport function normalizeOAIName(name: string | undefined): string | undefined {\n return name?.toLowerCase().replace(/[\\s!-/:-@[-`{-~]/g, '') || undefined;\n}\n\nexport type InitConfig = {\n pixelId: string;\n /** Log SDK activity to the browser console. */\n debug?: boolean;\n user?: OAIQUser;\n};\n\n/**\n * Fourth argument of `oaiq(\"measure\", ...)`. Required for custom events (carries\n * `custom_event_name`); optional for standard events.\n */\nexport type MeasureOptions = {\n /** Shared with the Conversions API to deduplicate the same conversion across browser and server. */\n event_id?: string;\n /** Required for custom events; 1-64 chars of letters, numbers, underscores, or dashes. */\n custom_event_name?: string;\n /** When true, opts the event out of user-level personalization. */\n opt_out?: boolean;\n};\n\nexport interface OAIQ {\n oaiq(command: 'init', config: InitConfig): void;\n oaiq<T extends StandardEvent>(\n command: 'measure',\n event: T,\n data: StandardEvents[T],\n options?: MeasureOptions\n ): void;\n oaiq(\n command: 'measure',\n event: 'custom',\n data: CustomEventData,\n options: MeasureOptions & { custom_event_name: string }\n ): void;\n}\n\n/**\n * Currencies whose minor unit has 0 or 3 decimal places; everything else uses 2.\n * Follows Stripe's zero-decimal convention (e.g., MGA is treated as 0, ISK is included),\n * which can differ from strict ISO 4217 exponents.\n * https://docs.stripe.com/currencies#zero-decimal\n */\nconst ZERO_DECIMAL_CURRENCIES = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'ISK',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n]);\nconst THREE_DECIMAL_CURRENCIES = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']);\n\n/**\n * Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None\n * without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every\n * `amount` — an amount alone would be invalid, not just imprecise.\n */\nexport function toMinorUnits(value?: number | null, currency?: string): number | undefined {\n if (value === undefined || value === null || Number.isNaN(value)) return undefined;\n const code = currency?.toUpperCase();\n if (!code) return undefined;\n const exponent =\n code && ZERO_DECIMAL_CURRENCIES.has(code)\n ? 0\n : code && THREE_DECIMAL_CURRENCIES.has(code)\n ? 3\n : 2;\n return Math.round(value * 10 ** exponent);\n}\n\nexport function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined {\n if (!items || items.length === 0) return undefined;\n const code = currency?.toUpperCase();\n return items.map((item) => {\n const amount = toMinorUnits(item.price, currency);\n return {\n id: item.item_id,\n name: item.item_name,\n content_type: item.item_category,\n quantity: item.quantity,\n amount,\n currency: amount !== undefined ? code : undefined,\n };\n });\n}\n\n/** Safely read a property off loosely typed track properties without widening to `any`. */\nfunction prop(properties: unknown, key: string): unknown {\n return typeof properties === 'object' && properties !== null\n ? (properties as Record<string, unknown>)[key]\n : undefined;\n}\n\nfunction readValue(properties: unknown): number | undefined {\n const value = prop(properties, 'value');\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction readCurrency(properties: unknown): string | undefined {\n const currency = prop(properties, 'currency');\n return typeof currency === 'string' ? currency : undefined;\n}\n\nfunction readItems(properties: unknown): Item[] | undefined {\n const items = prop(properties, 'items');\n return Array.isArray(items) ? (items as Item[]) : undefined;\n}\n\nfunction contentsData(properties: unknown): ContentsData {\n const currency = readCurrency(properties);\n return {\n type: 'contents',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n };\n}\n\nfunction customerActionData(properties: unknown): CustomerActionData {\n const currency = readCurrency(properties);\n return {\n type: 'customer_action',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n };\n}\n\nfunction planEnrollmentData(properties: unknown): PlanEnrollmentData {\n const currency = readCurrency(properties);\n const items = readItems(properties);\n return {\n type: 'plan_enrollment',\n plan_id: items?.[0]?.item_id,\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(items, currency),\n };\n}\n\nexport type MappedOAIEvent = {\n type: StandardEvent | 'custom';\n data: EventData;\n};\n\n/** Map an internal track event onto an OpenAI standard (or custom) conversion event. */\nexport function mapOAIEvent<T extends EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n): MappedOAIEvent {\n switch (name) {\n case 'page_view':\n return { type: 'page_viewed', data: contentsData(properties) };\n case 'view_item':\n case 'view_item_list':\n return { type: 'contents_viewed', data: contentsData(properties) };\n case 'begin_checkout':\n return { type: 'checkout_started', data: contentsData(properties) };\n case 'add_to_cart':\n return { type: 'items_added', data: contentsData(properties) };\n case 'purchase':\n return { type: 'order_created', data: contentsData(properties) };\n case 'generate_lead':\n case 'qualify_lead':\n return { type: 'lead_created', data: customerActionData(properties) };\n case 'sign_up':\n case 'login':\n return { type: 'registration_completed', data: customerActionData(properties) };\n case 'schedule':\n return { type: 'appointment_scheduled', data: customerActionData(properties) };\n case 'subscribe':\n return { type: 'subscription_created', data: planEnrollmentData(properties) };\n case 'trial_begin':\n return { type: 'trial_started', data: planEnrollmentData(properties) };\n default: {\n const currency = readCurrency(properties);\n return {\n type: 'custom',\n data: {\n type: 'custom',\n amount: toMinorUnits(readValue(properties), currency),\n currency: currency?.toUpperCase(),\n contents: mapContents(readItems(properties), currency),\n },\n };\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/track/oaiq.d.cts
CHANGED
|
@@ -59,20 +59,42 @@ export type StandardEvents = {
|
|
|
59
59
|
};
|
|
60
60
|
export type StandardEvent = keyof StandardEvents;
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
|
|
63
|
+
* starting and ending with a letter or digit, not a standard event name; the API lowercases it.
|
|
64
|
+
* Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when
|
|
65
|
+
* nothing usable is left — the event is then not sent at all. The pixel and the server both name
|
|
66
|
+
* a custom event through this, so the two still deduplicate.
|
|
67
|
+
* https://developers.openai.com/ads/supported-events
|
|
68
|
+
*/
|
|
69
|
+
export declare function oaiCustomEventName(name: string): string | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Identity fields passed to `oaiq("init", ...)` for conversion matching. Identifiers are
|
|
72
|
+
* normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.
|
|
73
|
+
* https://developers.openai.com/ads/measurement-pixel#send-user-data
|
|
65
74
|
*/
|
|
66
75
|
export type OAIQUser = {
|
|
67
76
|
email_sha256?: string;
|
|
77
|
+
/** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */
|
|
78
|
+
phone_number_sha256?: string;
|
|
68
79
|
external_id_sha256?: string;
|
|
80
|
+
/** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */
|
|
81
|
+
first_name_sha256?: string;
|
|
82
|
+
last_name_sha256?: string;
|
|
69
83
|
/** Two-letter ISO 3166-1 country code (e.g. "US"). */
|
|
70
84
|
country?: string;
|
|
71
|
-
/**
|
|
85
|
+
/** Max 128 characters; OpenAI trims and lowercases it. */
|
|
72
86
|
city?: string;
|
|
73
|
-
/**
|
|
74
|
-
|
|
87
|
+
/** State, province or region, max 128 characters. */
|
|
88
|
+
region?: string;
|
|
89
|
+
postal_code?: string;
|
|
75
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then
|
|
93
|
+
* the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.
|
|
94
|
+
*/
|
|
95
|
+
export declare function normalizeOAIPhone(phone: string): string | undefined;
|
|
96
|
+
/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */
|
|
97
|
+
export declare function normalizeOAIName(name: string | undefined): string | undefined;
|
|
76
98
|
export type InitConfig = {
|
|
77
99
|
pixelId: string;
|
|
78
100
|
/** Log SDK activity to the browser console. */
|
|
@@ -98,7 +120,11 @@ export interface OAIQ {
|
|
|
98
120
|
custom_event_name: string;
|
|
99
121
|
}): void;
|
|
100
122
|
}
|
|
101
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None
|
|
125
|
+
* without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every
|
|
126
|
+
* `amount` — an amount alone would be invalid, not just imprecise.
|
|
127
|
+
*/
|
|
102
128
|
export declare function toMinorUnits(value?: number | null, currency?: string): number | undefined;
|
|
103
129
|
export declare function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined;
|
|
104
130
|
export type MappedOAIEvent = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oaiq.d.cts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;
|
|
1
|
+
{"version":3,"file":"oaiq.d.cts","names":[],"sources":["../../src/track/oaiq.ts"],"mappings":";;;qBAGa;;;;;YAiBD;EACV;EACA;EACA;EACA;;EAEA;EACA;;YAGU;EACV;;EAEA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA,WAAW;;YAGD;EACV;EACA;EACA;EACA,WAAW;;YAGD,YAAY,eAAe,qBAAqB,qBAAqB;;;;;YAMrE;EACV,uBAAuB;EACvB,kBAAkB;EAClB,iBAAiB;EACjB,aAAa;EACb,cAAc;EACd,eAAe;EACf,aAAa;EACb,wBAAwB;EACxB,sBAAsB;EACtB,eAAe;;YAGL,sBAAsB;;;;;;;;;wBAuBlB,mBAAmB;;;;;;YAevB;EACV;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;;EAEA;EACA;;;;;;wBAOc,kBAAkB;;wBASlB,iBAAiB;YAIrB;EACV;;EAEA;EACA,OAAO;;;;;;YAOG;;EAEV;;EAEA;;EAEA;;iBAGe;EACf,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,UAAU,eACb,oBACA,OAAO,GACP,MAAM,eAAe,IACrB,UAAU;EAEZ,KACE,oBACA,iBACA,MAAM,iBACN,SAAS;IAAmB;;;;;;;;wBAoChB,aAAa,uBAAuB;wBAapC,YAAY,QAAQ,QAAQ,oBAAoB;YAqEpD;EACV,MAAM;EACN,MAAM;;;wBAIQ,YAAY,UAAU,WACpC,MAAM,UAAU,IAChB,aAAa,gBAAgB,KAC5B"}
|
package/dist/track/oaiq.d.mts
CHANGED
|
@@ -59,20 +59,42 @@ export type StandardEvents = {
|
|
|
59
59
|
};
|
|
60
60
|
export type StandardEvent = keyof StandardEvents;
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* A track name as an OpenAI `custom_event_name`: 1–64 letters, digits, underscores or hyphens,
|
|
63
|
+
* starting and ending with a letter or digit, not a standard event name; the API lowercases it.
|
|
64
|
+
* Anything else becomes an underscore, so `Sign Up.Clicked` is `sign_up_clicked`. Undefined when
|
|
65
|
+
* nothing usable is left — the event is then not sent at all. The pixel and the server both name
|
|
66
|
+
* a custom event through this, so the two still deduplicate.
|
|
67
|
+
* https://developers.openai.com/ads/supported-events
|
|
68
|
+
*/
|
|
69
|
+
export declare function oaiCustomEventName(name: string): string | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Identity fields passed to `oaiq("init", ...)` for conversion matching. Identifiers are
|
|
72
|
+
* normalized, then sent as lowercase 64-char SHA-256 hex strings; geographic values are raw.
|
|
73
|
+
* https://developers.openai.com/ads/measurement-pixel#send-user-data
|
|
65
74
|
*/
|
|
66
75
|
export type OAIQUser = {
|
|
67
76
|
email_sha256?: string;
|
|
77
|
+
/** 8–15 digits with the country code, no leading `+` or zeroes (`normalizeOAIPhone`). */
|
|
78
|
+
phone_number_sha256?: string;
|
|
68
79
|
external_id_sha256?: string;
|
|
80
|
+
/** Lowercase, without whitespace or ASCII punctuation (`normalizeOAIName`). */
|
|
81
|
+
first_name_sha256?: string;
|
|
82
|
+
last_name_sha256?: string;
|
|
69
83
|
/** Two-letter ISO 3166-1 country code (e.g. "US"). */
|
|
70
84
|
country?: string;
|
|
71
|
-
/**
|
|
85
|
+
/** Max 128 characters; OpenAI trims and lowercases it. */
|
|
72
86
|
city?: string;
|
|
73
|
-
/**
|
|
74
|
-
|
|
87
|
+
/** State, province or region, max 128 characters. */
|
|
88
|
+
region?: string;
|
|
89
|
+
postal_code?: string;
|
|
75
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* A phone number as OpenAI hashes it: whitespace, parentheses, periods and hyphens removed, then
|
|
93
|
+
* the leading `+` and zeroes; 8–15 digits with the country code, else not a usable number.
|
|
94
|
+
*/
|
|
95
|
+
export declare function normalizeOAIPhone(phone: string): string | undefined;
|
|
96
|
+
/** A name as OpenAI hashes it: lowercase, no whitespace or ASCII punctuation, accents kept. */
|
|
97
|
+
export declare function normalizeOAIName(name: string | undefined): string | undefined;
|
|
76
98
|
export type InitConfig = {
|
|
77
99
|
pixelId: string;
|
|
78
100
|
/** Log SDK activity to the browser console. */
|
|
@@ -98,7 +120,11 @@ export interface OAIQ {
|
|
|
98
120
|
custom_event_name: string;
|
|
99
121
|
}): void;
|
|
100
122
|
}
|
|
101
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Convert a major-unit amount (e.g., 129.99) into ISO 4217 minor units (e.g., 12,999). None
|
|
125
|
+
* without a currency: the minor unit depends on it, and OpenAI requires a `currency` with every
|
|
126
|
+
* `amount` — an amount alone would be invalid, not just imprecise.
|
|
127
|
+
*/
|
|
102
128
|
export declare function toMinorUnits(value?: number | null, currency?: string): number | undefined;
|
|
103
129
|
export declare function mapContents(items?: Item[], currency?: string): ContentItem[] | undefined;
|
|
104
130
|
export type MappedOAIEvent = {
|