Compare commits
6 Commits
93e5c35d86
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
354a2ac739 | ||
|
|
ac3aabc1ed | ||
|
|
2daf2eabfb | ||
|
|
e3c880c17b | ||
|
|
01fd81f911 | ||
|
|
af1b66649b |
@@ -4,7 +4,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.auvem.com/svelte-toolkit/spectator.git"
|
"url": "https://gitea.auvem.com/svelte-toolkit/spectator.git"
|
||||||
},
|
},
|
||||||
"version": "0.0.6",
|
"version": "0.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dev) log.info('[Umami] [dev]: tracking disabled');
|
if (dev) log.info('[Umami] [dev]: reporting disabled');
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (dev) {
|
if (dev) {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export class CAPIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends CAPIEvents to the server endpoint.
|
* Sends CAPIEvents to the server endpoint. If no consent is given, no events
|
||||||
|
* are sent and a dummy response is returned.
|
||||||
* @param events - The array of CAPIEvents to send.
|
* @param events - The array of CAPIEvents to send.
|
||||||
* @returns A promise that resolves to the server response.
|
* @returns A promise that resolves to the server response.
|
||||||
* @throws Will throw an error if input or response shape validation fails.
|
* @throws Will throw an error if input or response shape validation fails.
|
||||||
@@ -43,10 +44,8 @@ export class CAPIClient {
|
|||||||
console.warn(`[CAPIClient] Consent not given. Skipping sending ${events.length} event(s).`);
|
console.warn(`[CAPIClient] Consent not given. Skipping sending ${events.length} event(s).`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pixelID: '',
|
fbtrace_id: '',
|
||||||
fbTraceID: '',
|
events_received: 0,
|
||||||
receivedEvents: 0,
|
|
||||||
processedEvents: 0,
|
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -62,7 +61,11 @@ export class CAPIClient {
|
|||||||
return e.toObject();
|
return e.toObject();
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
try {
|
||||||
v.parse(v.object({ events: v.array(v.any()) }), body); // Validate body shape
|
v.parse(v.object({ events: v.array(v.any()) }), body); // Validate body shape
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`[CAPIClient] Invalid request body shape: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(this._href, {
|
const response = await fetch(this._href, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -73,6 +76,7 @@ export class CAPIClient {
|
|||||||
});
|
});
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
|
try {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const parsed = v.parse(capiResponseBodySchema, json);
|
const parsed = v.parse(capiResponseBodySchema, json);
|
||||||
return parsed as CAPIResponseBody;
|
return parsed as CAPIResponseBody;
|
||||||
@@ -80,6 +84,9 @@ export class CAPIClient {
|
|||||||
const parsed = v.parse(capiErrorBodySchema, json);
|
const parsed = v.parse(capiErrorBodySchema, json);
|
||||||
return parsed as CAPIErrorBody;
|
return parsed as CAPIErrorBody;
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`[CAPIClient] Invalid response shape: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import type { CAPIEvent } from './event.ts';
|
import type { CAPIEvent } from './event.ts';
|
||||||
|
import * as v from 'valibot';
|
||||||
|
import { capiResponseBodySchema, type CAPIResponseBody } from './handle.ts';
|
||||||
|
|
||||||
const GRAPH_VERSION = 'v24.0';
|
const GRAPH_VERSION = 'v24.0';
|
||||||
|
|
||||||
/**
|
|
||||||
* Response body from Meta Conversion API after sending events.
|
|
||||||
*/
|
|
||||||
export type CAPIRequestResponseBody = {
|
|
||||||
/** Dataset or Pixel ID to which the event successfully posted. */
|
|
||||||
pixelID: string;
|
|
||||||
/** fbtrace_id for debugging purposes. */
|
|
||||||
fbtrace_id: string;
|
|
||||||
/** Number of events received that were sent by the request. */
|
|
||||||
receivedEvents: number;
|
|
||||||
/** Number of events successfully posted by the request. */
|
|
||||||
processedEvents: number;
|
|
||||||
/** Messages returned by the server. */
|
|
||||||
messages: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connector class for Meta Conversion API (CAPI). Abstraction over direct HTTP
|
* Connector class for Meta Conversion API (CAPI). Abstraction over direct HTTP
|
||||||
* requests to Meta's CAPI endpoint.
|
* requests to Meta's CAPI endpoint.
|
||||||
@@ -52,7 +38,7 @@ export class CAPIConnector {
|
|||||||
* @returns The response from the Meta CAPI.
|
* @returns The response from the Meta CAPI.
|
||||||
* @throws Will throw an error if the request fails or the API returns an error.
|
* @throws Will throw an error if the request fails or the API returns an error.
|
||||||
*/
|
*/
|
||||||
async sendEvents(events: CAPIEvent[]): Promise<CAPIRequestResponseBody> {
|
async sendEvents(events: CAPIEvent[]): Promise<CAPIResponseBody> {
|
||||||
if (dev && !this._testEventCode) {
|
if (dev && !this._testEventCode) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`[CAPIConnector] Sending ${events.length} event(s) in dev mode without a test event code. ` +
|
`[CAPIConnector] Sending ${events.length} event(s) in dev mode without a test event code. ` +
|
||||||
@@ -60,14 +46,17 @@ export class CAPIConnector {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://graph.facebook.com/${GRAPH_VERSION}/${this._pixelID}/events?access_token=${this._accessToken}`;
|
const url = `https://graph.facebook.com/${GRAPH_VERSION}/${this._pixelID}/events`;
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
data: events.map((e) => e.toObject()),
|
data: events.map((e) => e.toObject()),
|
||||||
test_event_code: this._testEventCode
|
test_event_code: this._testEventCode
|
||||||
};
|
};
|
||||||
|
log.debug(
|
||||||
|
`[CAPIConnector] [${this._pixelID}] Sending ${events.length} event(s) to Meta CAPI at ${url} with body: ${JSON.stringify(body, null, 2)}`
|
||||||
|
);
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(`${url}?access_token=${this._accessToken}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -75,13 +64,20 @@ export class CAPIConnector {
|
|||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
const json = await resp.json();
|
const json = await resp.json();
|
||||||
console.log('CAPI response:', json);
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw new Error(`Meta CAPI error ${resp.status}: ${JSON.stringify(json, null, 2)}`);
|
throw new Error(`Meta CAPI error ${resp.status}: ${JSON.stringify(json, null, 2)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {} as CAPIRequestResponseBody;
|
try {
|
||||||
|
const parsed = v.parse(capiResponseBodySchema, json);
|
||||||
|
log.info(
|
||||||
|
`[CAPIConnector] [${this._pixelID}] Successfully sent ${events.length} event(s) to Meta CAPI.`
|
||||||
|
);
|
||||||
|
return parsed as CAPIResponseBody;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`[CAPIConnector] Invalid response shape: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,7 +87,7 @@ export class CAPIConnector {
|
|||||||
* @returns The response from the Meta CAPI.
|
* @returns The response from the Meta CAPI.
|
||||||
* @throws Will throw an error if the request fails or the API returns an error.
|
* @throws Will throw an error if the request fails or the API returns an error.
|
||||||
*/
|
*/
|
||||||
async trackEvent(event: CAPIEvent): Promise<CAPIRequestResponseBody> {
|
async trackEvent(event: CAPIEvent): Promise<CAPIResponseBody> {
|
||||||
return this.sendEvents([event]);
|
return this.sendEvents([event]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { StandardEventName } from '../types/fbq.js';
|
import type { StandardEventName } from '../types/fbq.js';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
const sha256 = (value: string): string => {
|
const sha256 = (value: string): string => {
|
||||||
return crypto.createHash('sha256').update(value).digest('hex');
|
return crypto.createHash('sha256').update(value).digest('hex');
|
||||||
@@ -202,7 +203,11 @@ export type CAPIEventOptions = {
|
|||||||
eventTime?: Date;
|
eventTime?: Date;
|
||||||
userData: CAPICustomerInfoParams;
|
userData: CAPICustomerInfoParams;
|
||||||
customData?: CAPIStandardParams;
|
customData?: CAPIStandardParams;
|
||||||
/** Required if actionSource is set to 'website' */
|
/**
|
||||||
|
* Required by Meta if actionSource is set to 'website'. If not provided and
|
||||||
|
* actionSource is 'website', a warning will be emitted and the field may be
|
||||||
|
* filled by the server receiving the event, however, accuracy is not guaranteed.
|
||||||
|
*/
|
||||||
eventSourceURL?: string;
|
eventSourceURL?: string;
|
||||||
optOut?: boolean;
|
optOut?: boolean;
|
||||||
eventID?: string;
|
eventID?: string;
|
||||||
@@ -220,6 +225,12 @@ export class CAPIEvent {
|
|||||||
return this._params;
|
return this._params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns human-readable event name, optionally including event ID. */
|
||||||
|
get readableName() {
|
||||||
|
if (!this._params) return 'Unknown Event';
|
||||||
|
return `${this._params.event_name}${this._params.event_id ? ` (${this._params.event_id})` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -249,9 +260,28 @@ export class CAPIEvent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
event._params = v.parse(MetaServerEventParamsSchema, event._params);
|
event._params = v.parse(MetaServerEventParamsSchema, event._params);
|
||||||
|
event.paramIntegrationCheck();
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs integration checks on the event parameters and logs warnings
|
||||||
|
* if potential issues are detected.
|
||||||
|
*/
|
||||||
|
private paramIntegrationCheck() {
|
||||||
|
if (
|
||||||
|
this._params &&
|
||||||
|
this._params.action_source === ActionSource.Website &&
|
||||||
|
!this._params.event_source_url
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`[CAPIEvent] Warning: ${this.readableName} ` +
|
||||||
|
`with actionSource 'website' is missing eventSourceURL. Provide eventSourceURL to improve ` +
|
||||||
|
`data quality and avoid blocks by Meta.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unmashals a JSON string or object into a CAPIEvent instance and parses
|
* Unmashals a JSON string or object into a CAPIEvent instance and parses
|
||||||
* its parameters to match Meta's expected shape.
|
* its parameters to match Meta's expected shape.
|
||||||
@@ -265,6 +295,7 @@ export class CAPIEvent {
|
|||||||
const parsed = v.parse(MetaServerEventParamsSchema, obj);
|
const parsed = v.parse(MetaServerEventParamsSchema, obj);
|
||||||
const event = new CAPIEvent();
|
const event = new CAPIEvent();
|
||||||
event._params = parsed;
|
event._params = parsed;
|
||||||
|
event.paramIntegrationCheck();
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +340,8 @@ export class CAPIEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enriches the CAPIEvent with additional user data.
|
* Enriches the CAPIEvent with additional user data. Will not override
|
||||||
|
* existing user data fields if already set.
|
||||||
* @param additionalData - Additional user data to merge.
|
* @param additionalData - Additional user data to merge.
|
||||||
* @throws Will throw an error if the enriched data fails validation.
|
* @throws Will throw an error if the enriched data fails validation.
|
||||||
*/
|
*/
|
||||||
@@ -326,4 +358,25 @@ export class CAPIEvent {
|
|||||||
// Re-validate after enrichment
|
// Re-validate after enrichment
|
||||||
this._params = v.parse(MetaServerEventParamsSchema, this._params);
|
this._params = v.parse(MetaServerEventParamsSchema, this._params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches the CAPIEvent with an event source URL. Will not override
|
||||||
|
* existing event source URL if already set AND will only apply if the
|
||||||
|
* action source is 'website'. A message will be logged to warn the user
|
||||||
|
* that an implicit event source URL is being set.
|
||||||
|
* @param url - The event source URL to set.
|
||||||
|
*/
|
||||||
|
unsafeEventSourceURL(url?: string) {
|
||||||
|
if (!this._params) return;
|
||||||
|
if (!url) {
|
||||||
|
log.warn(
|
||||||
|
`[CAPIEvent] ${this.readableName} could not implicitly set eventSourceURL, event will be submitted without it.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this._params.event_source_url && this._params.action_source === ActionSource.Website) {
|
||||||
|
this._params.event_source_url = url;
|
||||||
|
log.warn(`[CAPIEvent] ${this.readableName} implicitly setting eventSourceURL to ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ export const capiErrorBodySchema = v.object({
|
|||||||
export type CAPIErrorBody = v.InferOutput<typeof capiErrorBodySchema>;
|
export type CAPIErrorBody = v.InferOutput<typeof capiErrorBodySchema>;
|
||||||
|
|
||||||
export const capiResponseBodySchema = v.object({
|
export const capiResponseBodySchema = v.object({
|
||||||
pixelID: v.string(),
|
fbtrace_id: v.string(),
|
||||||
fbTraceID: v.string(),
|
events_received: v.number(),
|
||||||
receivedEvents: v.number(),
|
|
||||||
processedEvents: v.number(),
|
|
||||||
messages: v.array(v.string())
|
messages: v.array(v.string())
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,13 +38,14 @@ export type CAPIResponseBody = v.InferOutput<typeof capiResponseBodySchema>;
|
|||||||
export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = (connector) => {
|
export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = (connector) => {
|
||||||
const handle: RequestHandler = async ({ request, getClientAddress, cookies }) => {
|
const handle: RequestHandler = async ({ request, getClientAddress, cookies }) => {
|
||||||
try {
|
try {
|
||||||
const json = await request.json();
|
const jsonBody = await request.json();
|
||||||
const parsed = v.parse(capiRequestBodySchema, json);
|
const parsed = v.parse(capiRequestBodySchema, jsonBody);
|
||||||
|
|
||||||
// Build enriched user data with IP, user agent, and fbp/fbc from cookies
|
// Build enriched user data with IP, user agent, and fbp/fbc from cookies
|
||||||
const ip = getRequestIP(request, getClientAddress);
|
const ip = getRequestIP(request, getClientAddress);
|
||||||
const ua = request.headers.get('user-agent') ?? undefined;
|
const ua = request.headers.get('user-agent') ?? undefined;
|
||||||
const { fbp, fbc } = getFbpFbcFromCookies(cookies);
|
const { fbp, fbc } = getFbpFbcFromCookies(cookies);
|
||||||
|
const eventSourceURL = request.headers.get('referer') ?? undefined;
|
||||||
|
|
||||||
const enrichedUserData: Partial<CAPICustomerInfoParams> = {
|
const enrichedUserData: Partial<CAPICustomerInfoParams> = {
|
||||||
clientIP: ip,
|
clientIP: ip,
|
||||||
@@ -55,10 +54,11 @@ export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = (
|
|||||||
fbc
|
fbc
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enrich each event's user data
|
// Enrich each event's user data & event source URL
|
||||||
const events: CAPIEvent[] = parsed.events.map((eventParams) => {
|
const events: CAPIEvent[] = parsed.events.map((eventParams) => {
|
||||||
const event = CAPIEvent.fromObject(eventParams);
|
const event = CAPIEvent.fromObject(eventParams);
|
||||||
event.enrichUserData(enrichedUserData);
|
event.enrichUserData(enrichedUserData);
|
||||||
|
event.unsafeEventSourceURL(eventSourceURL);
|
||||||
return event;
|
return event;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export * from './util/ip.ts';
|
|||||||
// set log level to debug if we're in dev mode
|
// set log level to debug if we're in dev mode
|
||||||
if (dev) {
|
if (dev) {
|
||||||
log.setLevel('debug');
|
log.setLevel('debug');
|
||||||
|
log.debug('[spectator] Log level set to debug');
|
||||||
} else {
|
} else {
|
||||||
log.setLevel('warn');
|
log.setLevel('warn');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import { resolveGetter, type MaybeGetter } from '../util/getter.ts';
|
|||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { CAPIClient } from '../capi/client.ts';
|
import { CAPIClient } from '../capi/client.ts';
|
||||||
import { ActionSource, CAPIEvent, type CAPIStandardParams } from '../capi/event.ts';
|
import {
|
||||||
|
ActionSource,
|
||||||
|
CAPIEvent,
|
||||||
|
type CAPIEventOptions,
|
||||||
|
type CAPIStandardParams
|
||||||
|
} from '../capi/event.ts';
|
||||||
|
|
||||||
const pixelParamsToCustomData = (params: CommonParams & CustomParams): CAPIStandardParams => {
|
const pixelParamsToCustomData = (params: CommonParams & CustomParams): CAPIStandardParams => {
|
||||||
const customData: CAPIStandardParams = {};
|
const customData: CAPIStandardParams = {};
|
||||||
@@ -101,7 +106,7 @@ export class PixelControl {
|
|||||||
this._conversionClient = new CAPIClient(options.conversionHref, resolvedTrackingManager);
|
this._conversionClient = new CAPIClient(options.conversionHref, resolvedTrackingManager);
|
||||||
} else if (options?.conversionHref) {
|
} else if (options?.conversionHref) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`[PixelControl] Conversion Client ${options.conversionHref} for Meta Pixel [${this._pixelID}] not initialized, TrackingManager is required for user consent.`
|
`${this.logPrefix} CAPI Client ${options.conversionHref} available but not initialized, TrackingManager is required for user consent.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +155,7 @@ export class PixelControl {
|
|||||||
// Check for existing PixelControl instance
|
// Check for existing PixelControl instance
|
||||||
if (this._registeredPixels[pixelID]) {
|
if (this._registeredPixels[pixelID]) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`[PixelControl] Instance for Meta Pixel ID: ${pixelID} already exists. Returning existing instance.`
|
`${this._registeredPixels[pixelID].logPrefix} Instance already exists. Returning existing instance.`
|
||||||
);
|
);
|
||||||
return this._registeredPixels[pixelID];
|
return this._registeredPixels[pixelID];
|
||||||
}
|
}
|
||||||
@@ -161,7 +166,7 @@ export class PixelControl {
|
|||||||
|
|
||||||
// Fire initialization
|
// Fire initialization
|
||||||
window.fbq('init', pixel._pixelID, options?.advancedMatching, options?.initOptions);
|
window.fbq('init', pixel._pixelID, options?.advancedMatching, options?.initOptions);
|
||||||
log.debug(`[PixelControl] [${pixel._pixelID}] initialized.`);
|
log.debug(`${pixel.logPrefix} initialized.`);
|
||||||
|
|
||||||
return pixel;
|
return pixel;
|
||||||
}
|
}
|
||||||
@@ -192,6 +197,21 @@ export class PixelControl {
|
|||||||
return trackingManager?.haveUserConsent() ?? false;
|
return trackingManager?.haveUserConsent() ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Warns if we're in dev mode and no test code is set */
|
||||||
|
private devModeWarn() {
|
||||||
|
if (dev && !this._testEventCode) {
|
||||||
|
log.warn(
|
||||||
|
`${this.logPrefix} Sending events in dev mode without a test event code. ` +
|
||||||
|
'Consider providing a test event code to avoid affecting real data.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the log prefix including the pixel ID */
|
||||||
|
private get logPrefix(): string {
|
||||||
|
return `[PixelControl] [${this._pixelID}]`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shorthand utility to send a PageView event
|
* Shorthand utility to send a PageView event
|
||||||
* @param disableCAPI If true, disables sending this event to the Conversion API
|
* @param disableCAPI If true, disables sending this event to the Conversion API
|
||||||
@@ -220,22 +240,28 @@ export class PixelControl {
|
|||||||
eventID = crypto.randomUUID();
|
eventID = crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._conversionClient
|
const opts: CAPIEventOptions = {
|
||||||
.trackEvent(
|
|
||||||
CAPIEvent.fromOpts({
|
|
||||||
eventName: event,
|
eventName: event,
|
||||||
eventID: eventID,
|
eventID: eventID,
|
||||||
actionSource: ActionSource.Website,
|
actionSource: ActionSource.Website,
|
||||||
|
eventSourceURL: window.location.href,
|
||||||
eventTime: new Date(),
|
eventTime: new Date(),
|
||||||
userData: {
|
userData: {
|
||||||
clientUserAgent: navigator.userAgent
|
clientUserAgent: navigator.userAgent
|
||||||
},
|
},
|
||||||
customData: params ? pixelParamsToCustomData(params) : undefined
|
customData: params ? pixelParamsToCustomData(params) : undefined
|
||||||
})
|
};
|
||||||
)
|
const ev = CAPIEvent.fromOpts(opts);
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
`${this.logPrefix} ${ev.readableName} forwarding to CAPI with body: ${JSON.stringify(ev.params, null, 2)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this._conversionClient
|
||||||
|
.trackEvent(ev)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
log.debug(
|
log.debug(
|
||||||
`[PixelControl] [${this._pixelID}] ${event} event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
|
`${this.logPrefix} ${ev.readableName} forwarded to CAPI, response: ${JSON.stringify(
|
||||||
response,
|
response,
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
@@ -243,11 +269,20 @@ export class PixelControl {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
log.error(
|
log.error(`${this.logPrefix} ${ev.readableName} failed to forward to CAPI. Error: `, error);
|
||||||
`[PixelControl] [${this._pixelID}] Failed to send ${event} event to Conversion API with Event ID: ${eventID}`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return eventID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEventReadableName(
|
||||||
|
event: StandardEventName | string,
|
||||||
|
eventID?: string,
|
||||||
|
testCode?: string
|
||||||
|
): string {
|
||||||
|
return (
|
||||||
|
event + (eventID ? ` (${eventID})` : '') + (testCode ? ` [test_event_code: ${testCode}]` : '')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -265,6 +300,7 @@ export class PixelControl {
|
|||||||
disableCAPI: boolean = false
|
disableCAPI: boolean = false
|
||||||
) {
|
) {
|
||||||
if (!this.consentGuard()) return;
|
if (!this.consentGuard()) return;
|
||||||
|
this.devModeWarn();
|
||||||
|
|
||||||
// Optionally, send to conversion API endpoint
|
// Optionally, send to conversion API endpoint
|
||||||
if (!disableCAPI) {
|
if (!disableCAPI) {
|
||||||
@@ -272,19 +308,13 @@ export class PixelControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send the event to Meta via the pixel
|
// Send the event to Meta via the pixel
|
||||||
if (!dev || this._testEventCode) {
|
|
||||||
window.fbq('trackSingle', this._pixelID, event, params, {
|
window.fbq('trackSingle', this._pixelID, event, params, {
|
||||||
eventID,
|
eventID,
|
||||||
test_event_code: this._testEventCode
|
test_event_code: this._testEventCode
|
||||||
});
|
});
|
||||||
log.debug(
|
log.debug(
|
||||||
`[PixelControl] [${this._pixelID}] ${event} event sent${dev && ` (test code: ${this._testEventCode})`}.`
|
`${this.logPrefix} ${this.getEventReadableName(event, eventID, this._testEventCode)} sent.`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
log.info(
|
|
||||||
`[PixelControl] [${this._pixelID}] ${event} event not sent in development mode without a test event code.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -302,25 +332,20 @@ export class PixelControl {
|
|||||||
disableCAPI: boolean = false
|
disableCAPI: boolean = false
|
||||||
) {
|
) {
|
||||||
if (!this.consentGuard()) return;
|
if (!this.consentGuard()) return;
|
||||||
|
this.devModeWarn();
|
||||||
|
|
||||||
// Optionally, send to conversdion API endpoint
|
// Optionally, send to conversion API endpoint
|
||||||
if (!disableCAPI) {
|
if (!disableCAPI) {
|
||||||
eventID = this.forwardToCAPI(event, params, eventID);
|
eventID = this.forwardToCAPI(event, params, eventID);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the event to Meta via the pixel
|
// Send the event to Meta via the pixel
|
||||||
if (!dev || this._testEventCode) {
|
|
||||||
window.fbq('trackSingleCustom', this._pixelID, event, params, {
|
window.fbq('trackSingleCustom', this._pixelID, event, params, {
|
||||||
eventID,
|
eventID,
|
||||||
test_event_code: this._testEventCode
|
test_event_code: this._testEventCode
|
||||||
});
|
});
|
||||||
log.debug(
|
log.debug(
|
||||||
`[PixelControl] [${this._pixelID}] ${event} custom event sent (test code: ${this._testEventCode}).`
|
`${this.logPrefix} ${this.getEventReadableName(event, eventID, this._testEventCode)} sent.`
|
||||||
);
|
|
||||||
} else {
|
|
||||||
log.info(
|
|
||||||
`[PixelControl] [${this._pixelID}] ${event} custom event not sent in development mode without a test event code.`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export class TrackingManager {
|
|||||||
private _consent: boolean | null = $state(null);
|
private _consent: boolean | null = $state(null);
|
||||||
private _services: Record<string, InternalService<unknown>> = {};
|
private _services: Record<string, InternalService<unknown>> = {};
|
||||||
private _changeCallbacks: Array<(consent: boolean | null) => void> = [];
|
private _changeCallbacks: Array<(consent: boolean | null) => void> = [];
|
||||||
|
private _loadCallbacks: Array<(consent: boolean | null) => void> = [];
|
||||||
|
private _loaded: boolean = false;
|
||||||
private _consentQueue: Array<() => void> = [];
|
private _consentQueue: Array<() => void> = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,6 +82,12 @@ export class TrackingManager {
|
|||||||
log.debug('[TrackingManager] Loaded tracking options from storage:', opts);
|
log.debug('[TrackingManager] Loaded tracking options from storage:', opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run load callbacks
|
||||||
|
this._loadCallbacks.forEach((cb) => {
|
||||||
|
cb(this._consent);
|
||||||
|
});
|
||||||
|
this._loaded = true;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +160,16 @@ export class TrackingManager {
|
|||||||
this.saveOpts();
|
this.saveOpts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback to be called only once the tracking state has been
|
||||||
|
* loaded from localStorage. Will be called immediately if already loaded
|
||||||
|
* and with each subsequent load.
|
||||||
|
*/
|
||||||
|
onceLoaded(callback: (consent: boolean | null) => void) {
|
||||||
|
this._loadCallbacks.push(callback);
|
||||||
|
if (this._loaded) callback(this.consent);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a callback to be notified when the tracking permission changes.
|
* Registers a callback to be notified when the tracking permission changes.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user