Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
354a2ac739 | ||
|
|
ac3aabc1ed | ||
|
|
2daf2eabfb |
@@ -4,7 +4,7 @@
|
||||
"type": "git",
|
||||
"url": "https://gitea.auvem.com/svelte-toolkit/spectator.git"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { StandardEventName } from '../types/fbq.js';
|
||||
import * as v from 'valibot';
|
||||
import crypto from 'node:crypto';
|
||||
import log from 'loglevel';
|
||||
|
||||
const sha256 = (value: string): string => {
|
||||
return crypto.createHash('sha256').update(value).digest('hex');
|
||||
@@ -202,7 +203,11 @@ export type CAPIEventOptions = {
|
||||
eventTime?: Date;
|
||||
userData: CAPICustomerInfoParams;
|
||||
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;
|
||||
optOut?: boolean;
|
||||
eventID?: string;
|
||||
@@ -220,6 +225,12 @@ export class CAPIEvent {
|
||||
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() {}
|
||||
|
||||
/**
|
||||
@@ -249,9 +260,28 @@ export class CAPIEvent {
|
||||
};
|
||||
|
||||
event._params = v.parse(MetaServerEventParamsSchema, event._params);
|
||||
event.paramIntegrationCheck();
|
||||
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
|
||||
* its parameters to match Meta's expected shape.
|
||||
@@ -265,6 +295,7 @@ export class CAPIEvent {
|
||||
const parsed = v.parse(MetaServerEventParamsSchema, obj);
|
||||
const event = new CAPIEvent();
|
||||
event._params = parsed;
|
||||
event.paramIntegrationCheck();
|
||||
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.
|
||||
* @throws Will throw an error if the enriched data fails validation.
|
||||
*/
|
||||
@@ -326,4 +358,25 @@ export class CAPIEvent {
|
||||
// Re-validate after enrichment
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = (
|
||||
const ip = getRequestIP(request, getClientAddress);
|
||||
const ua = request.headers.get('user-agent') ?? undefined;
|
||||
const { fbp, fbc } = getFbpFbcFromCookies(cookies);
|
||||
const eventSourceURL = request.headers.get('referer') ?? undefined;
|
||||
|
||||
const enrichedUserData: Partial<CAPICustomerInfoParams> = {
|
||||
clientIP: ip,
|
||||
@@ -53,10 +54,11 @@ export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = (
|
||||
fbc
|
||||
};
|
||||
|
||||
// Enrich each event's user data
|
||||
// Enrich each event's user data & event source URL
|
||||
const events: CAPIEvent[] = parsed.events.map((eventParams) => {
|
||||
const event = CAPIEvent.fromObject(eventParams);
|
||||
event.enrichUserData(enrichedUserData);
|
||||
event.unsafeEventSourceURL(eventSourceURL);
|
||||
return event;
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,12 @@ import { resolveGetter, type MaybeGetter } from '../util/getter.ts';
|
||||
import log from 'loglevel';
|
||||
import { dev } from '$app/environment';
|
||||
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 customData: CAPIStandardParams = {};
|
||||
@@ -101,7 +106,7 @@ export class PixelControl {
|
||||
this._conversionClient = new CAPIClient(options.conversionHref, resolvedTrackingManager);
|
||||
} else if (options?.conversionHref) {
|
||||
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
|
||||
if (this._registeredPixels[pixelID]) {
|
||||
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];
|
||||
}
|
||||
@@ -161,7 +166,7 @@ export class PixelControl {
|
||||
|
||||
// Fire initialization
|
||||
window.fbq('init', pixel._pixelID, options?.advancedMatching, options?.initOptions);
|
||||
log.debug(`[PixelControl] [${pixel._pixelID}] initialized.`);
|
||||
log.debug(`${pixel.logPrefix} initialized.`);
|
||||
|
||||
return pixel;
|
||||
}
|
||||
@@ -196,12 +201,17 @@ export class PixelControl {
|
||||
private devModeWarn() {
|
||||
if (dev && !this._testEventCode) {
|
||||
log.warn(
|
||||
`[PixelControl] [${this._pixelID}] Sending events in dev mode without a test event code. ` +
|
||||
`${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
|
||||
* @param disableCAPI If true, disables sending this event to the Conversion API
|
||||
@@ -230,22 +240,28 @@ export class PixelControl {
|
||||
eventID = crypto.randomUUID();
|
||||
}
|
||||
|
||||
const opts: CAPIEventOptions = {
|
||||
eventName: event,
|
||||
eventID: eventID,
|
||||
actionSource: ActionSource.Website,
|
||||
eventSourceURL: window.location.href,
|
||||
eventTime: new Date(),
|
||||
userData: {
|
||||
clientUserAgent: navigator.userAgent
|
||||
},
|
||||
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(
|
||||
CAPIEvent.fromOpts({
|
||||
eventName: event,
|
||||
eventID: eventID,
|
||||
actionSource: ActionSource.Website,
|
||||
eventTime: new Date(),
|
||||
userData: {
|
||||
clientUserAgent: navigator.userAgent
|
||||
},
|
||||
customData: params ? pixelParamsToCustomData(params) : undefined
|
||||
})
|
||||
)
|
||||
.trackEvent(ev)
|
||||
.then((response) => {
|
||||
log.debug(
|
||||
`[PixelControl] [${this._pixelID}] ${event} event forwarded to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
|
||||
`${this.logPrefix} ${ev.readableName} forwarded to CAPI, response: ${JSON.stringify(
|
||||
response,
|
||||
null,
|
||||
2
|
||||
@@ -253,15 +269,22 @@ export class PixelControl {
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(
|
||||
`[PixelControl] [${this._pixelID}] Failed to forward ${event} event to Conversion API with Event ID: ${eventID}`,
|
||||
error
|
||||
);
|
||||
log.error(`${this.logPrefix} ${ev.readableName} failed to forward to CAPI. Error: `, error);
|
||||
});
|
||||
|
||||
return eventID;
|
||||
}
|
||||
|
||||
private getEventReadableName(
|
||||
event: StandardEventName | string,
|
||||
eventID?: string,
|
||||
testCode?: string
|
||||
): string {
|
||||
return (
|
||||
event + (eventID ? ` (${eventID})` : '') + (testCode ? ` [test_event_code: ${testCode}]` : '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks a standard event for this pixel (uses `trackSingle` under the hood)
|
||||
* @param event Standard event name
|
||||
@@ -290,7 +313,7 @@ export class PixelControl {
|
||||
test_event_code: this._testEventCode
|
||||
});
|
||||
log.debug(
|
||||
`[PixelControl] [${this._pixelID}] ${event} event sent with event ID ${eventID} ${dev && ` (test code: ${this._testEventCode})`}.`
|
||||
`${this.logPrefix} ${this.getEventReadableName(event, eventID, this._testEventCode)} sent.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -322,7 +345,7 @@ export class PixelControl {
|
||||
test_event_code: this._testEventCode
|
||||
});
|
||||
log.debug(
|
||||
`[PixelControl] [${this._pixelID}] ${event} custom event sent with event ID ${eventID} ${dev && ` (test code: ${this._testEventCode})`}.`
|
||||
`${this.logPrefix} ${this.getEventReadableName(event, eventID, this._testEventCode)} sent.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user