Compare commits
15 Commits
68f3941f44
...
v0.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
674663b027 | ||
|
|
e7b12f50b1 | ||
|
|
927b02d30e | ||
|
|
45a6bda53e | ||
|
|
9a72280737 | ||
|
|
095462c80d | ||
|
|
bb92e25485 | ||
|
|
eeccb09b0b | ||
|
|
ff99579ff1 | ||
|
|
802a825854 | ||
|
|
af1c423ccb | ||
|
|
110f0d2434 | ||
|
|
51dfcfde59 | ||
|
|
ae08a564a8 | ||
|
|
04ce2d3c57 |
10
package.json
10
package.json
@@ -4,7 +4,8 @@
|
||||
"type": "git",
|
||||
"url": "https://gitea.auvem.com/svelte-toolkit/spectator.git"
|
||||
},
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.4",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && npm run prepack",
|
||||
@@ -34,12 +35,12 @@
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"svelte": "^5.40.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.49.1",
|
||||
"@sveltejs/package": "^2.5.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
@@ -61,7 +62,8 @@
|
||||
"svelte"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/umami": "^2.10.1"
|
||||
"@types/umami": "^2.10.1",
|
||||
"loglevel": "^1.9.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://gitea.auvem.com/api/packages/svelte-toolkit/npm/"
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@types/umami':
|
||||
specifier: ^2.10.1
|
||||
version: 2.10.1
|
||||
loglevel:
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.2
|
||||
devDependencies:
|
||||
'@eslint/compat':
|
||||
specifier: ^1.4.0
|
||||
@@ -18,9 +21,6 @@ importers:
|
||||
'@eslint/js':
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.2
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4)))(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4)))
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.49.1
|
||||
version: 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4)))(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4))
|
||||
@@ -432,11 +432,6 @@ packages:
|
||||
peerDependencies:
|
||||
acorn: ^8.9.0
|
||||
|
||||
'@sveltejs/adapter-auto@7.0.0':
|
||||
resolution: {integrity: sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==}
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.0.0
|
||||
|
||||
'@sveltejs/kit@2.49.2':
|
||||
resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==}
|
||||
engines: {node: '>=18.13'}
|
||||
@@ -846,6 +841,10 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
loglevel@1.9.2:
|
||||
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@@ -1393,10 +1392,6 @@ snapshots:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
||||
'@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4)))(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4)))(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4))
|
||||
|
||||
'@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4)))(svelte@5.46.0)(vite@7.3.0(@types/node@24.10.4))':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
@@ -1852,6 +1847,8 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
loglevel@1.9.2: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
<!-- @component
|
||||
MetaPixel integrates the Meta (Facebook) Pixel into your Svelte application,
|
||||
allowing you to track page views and custom events while respecting user consent
|
||||
for tracking. The component manages the lifecycle of the Meta Pixel script and
|
||||
PixelControl interface.
|
||||
|
||||
The PixelControl class also allows you to directly manage multiple Pixel
|
||||
instances and handle event tracking with optional test event codes without
|
||||
using the MetaPixel component.
|
||||
-->
|
||||
|
||||
<script lang="ts" module>
|
||||
export type PixelControlOptions = {
|
||||
/**
|
||||
* if provided, events fired will always have this code attached
|
||||
* to prevent them from polluting real analytics data.
|
||||
*/
|
||||
testEventCode?: string;
|
||||
/** Advanced matching data */
|
||||
advancedMatching?: AdvancedMatching;
|
||||
/** Initialization options */
|
||||
initOptions?: InitOptions;
|
||||
};
|
||||
|
||||
export class PixelControl {
|
||||
private _pixelID: string;
|
||||
private _testEventCode?: string = undefined;
|
||||
private _trackingManager: MaybeGetter<TrackingManager | undefined>;
|
||||
|
||||
private static _baseLoaded: boolean = false;
|
||||
private static _registeredPixels: Record<string, PixelControl> = {};
|
||||
|
||||
/** Indicates whether the Meta Pixel base script has been loaded. */
|
||||
static get baseLoaded(): boolean {
|
||||
return this._baseLoaded;
|
||||
}
|
||||
@@ -16,7 +41,7 @@
|
||||
* @throws Error if the Meta Pixel API is not loaded.
|
||||
*/
|
||||
static loadGuard(): void {
|
||||
if (!this._baseLoaded || !window._fbq) {
|
||||
if (!this._baseLoaded || !window.fbq) {
|
||||
throw new Error('Meta Pixel API has not been loaded. Call PixelControl.load() first.');
|
||||
}
|
||||
}
|
||||
@@ -24,55 +49,91 @@
|
||||
private constructor(
|
||||
trackingManager: MaybeGetter<TrackingManager | undefined>,
|
||||
pixelID: string,
|
||||
testEventCode?: string
|
||||
options?: PixelControlOptions
|
||||
) {
|
||||
this._trackingManager = trackingManager;
|
||||
this._pixelID = pixelID;
|
||||
this._testEventCode = testEventCode;
|
||||
this._testEventCode = options?.testEventCode;
|
||||
}
|
||||
|
||||
/** Loads the Meta Pixel base script. */
|
||||
static load() {
|
||||
if (this._baseLoaded && window._fbq) return;
|
||||
if (!window._fbq) {
|
||||
PixelControl.revokeConsent(); // Initialize without consent
|
||||
loadMetaPixel(); // Load the Meta Pixel script
|
||||
static async load() {
|
||||
if (this._baseLoaded && !!window.fbq) return;
|
||||
if (!window.fbq) {
|
||||
try {
|
||||
await loadMetaPixel(); // Load the Meta Pixel script
|
||||
} catch (e) {
|
||||
log.warn('Failed to load Meta Pixel script, all events will be queued.', e);
|
||||
}
|
||||
}
|
||||
this._baseLoaded = true;
|
||||
log.debug('Meta Pixel base script loaded.');
|
||||
}
|
||||
|
||||
/** Tells the Meta pixel that the user has given consent for tracking. */
|
||||
static grantConsent() {
|
||||
this.loadGuard();
|
||||
window.fbq?.('consent', 'grant');
|
||||
log.debug('Meta Pixel consent granted.');
|
||||
}
|
||||
|
||||
/** Tells the Meta pixel that the user has revoked consent for tracking. */
|
||||
static revokeConsent() {
|
||||
this.loadGuard();
|
||||
window.fbq?.('consent', 'revoke');
|
||||
log.debug('Meta Pixel consent revoked.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a PixelControl instance for the given Meta Pixel ID. If
|
||||
* Registers a PixelControl instance for the given Meta Pixel ID. If
|
||||
* the base Meta Pixel script has not been loaded yet, it will be
|
||||
* loaded automatically. Optionally sets a test event code for the Pixel.
|
||||
* Should only be called once for each Pixel ID, use PixelControl.get()
|
||||
* to retrieve existing instances.
|
||||
* @param trackingManager Tracking manager to handle user consent for tracking
|
||||
* @param pixelID Meta Pixel ID
|
||||
* @param testEventCode Optional test event code
|
||||
* @param options Optional settings
|
||||
* @returns PixelControl instance
|
||||
*/
|
||||
static for(
|
||||
static initialize(
|
||||
trackingManager: MaybeGetter<TrackingManager | undefined>,
|
||||
pixelID: string,
|
||||
options?: {
|
||||
testEventCode?: string;
|
||||
advancedMatching?: AdvancedMatching;
|
||||
initOptions?: InitOptions;
|
||||
}
|
||||
options?: PixelControlOptions
|
||||
): PixelControl {
|
||||
// Load the base script if not already loaded
|
||||
PixelControl.load();
|
||||
window.fbq('init', pixelID);
|
||||
return new PixelControl(trackingManager, pixelID, options?.testEventCode);
|
||||
|
||||
// Check for existing PixelControl instance
|
||||
if (this._registeredPixels[pixelID]) {
|
||||
log.warn(
|
||||
`PixelControl instance for Meta Pixel ID: ${pixelID} already exists. Returning existing instance.`
|
||||
);
|
||||
return this._registeredPixels[pixelID];
|
||||
}
|
||||
|
||||
// Create and register the PixelControl instance
|
||||
const pixel = new PixelControl(trackingManager, pixelID, options);
|
||||
this._registeredPixels[pixelID] = pixel;
|
||||
|
||||
// Fire initialization
|
||||
window.fbq('init', pixel._pixelID, options?.advancedMatching, options?.initOptions);
|
||||
log.debug(`Meta Pixel [${pixel._pixelID}] initialized.`);
|
||||
|
||||
return pixel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an existing PixelControl instance for the given Meta Pixel ID.
|
||||
* @param pixelID Meta Pixel ID
|
||||
* @returns PixelControl instance
|
||||
* @throws Error if no PixelControl instance is found for the given ID.
|
||||
*/
|
||||
static get(pixelID: string): PixelControl {
|
||||
const pixel = this._registeredPixels[pixelID];
|
||||
if (!pixel) {
|
||||
throw new Error(`No PixelControl instance found for Meta Pixel ID: ${pixelID}`);
|
||||
}
|
||||
return pixel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,6 +155,9 @@
|
||||
pageView() {
|
||||
if (!this.consentGuard()) return;
|
||||
window.fbq('track', 'PageView', undefined, { test_event_code: this._testEventCode });
|
||||
log.debug(
|
||||
`Meta Pixel [${this._pixelID}] PageView event sent (test code: ${this._testEventCode}).`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,6 +170,9 @@
|
||||
eventID,
|
||||
test_event_code: this._testEventCode
|
||||
});
|
||||
log.debug(
|
||||
`Meta Pixel [${this._pixelID}] ${event} event sent (test code: ${this._testEventCode}).`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,12 +185,15 @@
|
||||
eventID,
|
||||
test_event_code: this._testEventCode
|
||||
});
|
||||
log.debug(
|
||||
`Meta Pixel [${this._pixelID}] ${event} custom event sent (test code: ${this._testEventCode}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { createContext, onMount } from 'svelte';
|
||||
|
||||
import type { TrackingManager } from './tracking.svelte.ts';
|
||||
import type {
|
||||
@@ -137,32 +207,30 @@
|
||||
import { loadMetaPixel } from './util/meta-pixel-loader.ts';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import { resolveGetter, type MaybeGetter } from './util/getter.ts';
|
||||
import log from 'loglevel';
|
||||
|
||||
interface Props {
|
||||
/** Meta Pixel ID */
|
||||
pixelID: string;
|
||||
|
||||
/**
|
||||
* If a test event code is available, events fired will always have this
|
||||
* code attached to prevent them from polluting real analytics data.
|
||||
*/
|
||||
testEventCode?: string;
|
||||
|
||||
/**
|
||||
* Controls whether page views are automatically tracked by this
|
||||
* component (default: true).
|
||||
*/
|
||||
autoPageView?: boolean;
|
||||
|
||||
/**
|
||||
* Tracking manager to handle user consent for tracking. If omitted
|
||||
* tracking is disabled by default until consent is granted via
|
||||
* PixelControl.grantConsent().
|
||||
*/
|
||||
trackingManager?: TrackingManager;
|
||||
|
||||
/** Meta Pixel ID */
|
||||
pixelID: string;
|
||||
|
||||
/** Meta Pixel Options */
|
||||
pixelOptions?: PixelControlOptions;
|
||||
|
||||
/**
|
||||
* Controls whether page views are automatically tracked by this
|
||||
* component (default: true).
|
||||
*/
|
||||
autoPageView?: boolean;
|
||||
}
|
||||
|
||||
let { pixelID, testEventCode, autoPageView = true, trackingManager }: Props = $props();
|
||||
let { pixelID, pixelOptions, autoPageView = true, trackingManager }: Props = $props();
|
||||
|
||||
let pixel = $state<PixelControl | null>(null);
|
||||
|
||||
@@ -171,7 +239,7 @@
|
||||
throw new Error('MetaPixel component requires a TrackingManager to manage consent.');
|
||||
}
|
||||
PixelControl.load();
|
||||
pixel = PixelControl.for(trackingManager, pixelID, { testEventCode });
|
||||
pixel = PixelControl.initialize(trackingManager, pixelID, pixelOptions);
|
||||
|
||||
trackingManager.runWithConsent(() => {
|
||||
if (autoPageView && pixel) {
|
||||
@@ -187,4 +255,11 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const getPixelControl = (): PixelControl => {
|
||||
if (!pixel) {
|
||||
throw new Error('MetaPixel component has not been initialized yet, wait for onMount.');
|
||||
}
|
||||
return pixel;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
import type { TrackingManager } from './tracking.svelte.ts';
|
||||
import log from 'loglevel';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
@@ -27,11 +28,11 @@
|
||||
const devConsoleTag = $derived(`[dev][consent: ${consentGranted ? 'granted' : 'revoked'}]`);
|
||||
const devOverride = {
|
||||
track: (...args: unknown[]): Promise<string> | undefined => {
|
||||
console.log(`${devConsoleTag}: Track called with:`, ...args);
|
||||
log.debug(`${devConsoleTag}: Track called with:`, ...args);
|
||||
return undefined;
|
||||
},
|
||||
identify: (...args: unknown[]): Promise<void> => {
|
||||
console.log(`${devConsoleTag}: Identify called with:`, ...args);
|
||||
log.debug(`${devConsoleTag}: Identify called with:`, ...args);
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
@@ -61,7 +62,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
if (dev) console.log('[dev]: Umami tracking disabled');
|
||||
if (dev) log.debug('[dev]: Umami tracking disabled');
|
||||
|
||||
onMount(() => {
|
||||
if (dev) {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
// Reexport your entry components here
|
||||
|
||||
export * as fbq from './types/fbq.js';
|
||||
export * from './MetaPixel.svelte';
|
||||
import { dev } from '$app/environment';
|
||||
import log from 'loglevel';
|
||||
|
||||
export type * as fbq from './types/fbq.d.ts';
|
||||
export { default as MetaPixel, PixelControl } from './MetaPixel.svelte';
|
||||
export * from './tracking.svelte.ts';
|
||||
export * from './Umami.svelte';
|
||||
export { default as Umami } from './Umami.svelte';
|
||||
|
||||
// set log level to debug if we're in dev mode
|
||||
if (dev) {
|
||||
log.setLevel('debug');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { setContext, getContext, onDestroy } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import log from 'loglevel';
|
||||
import { onDestroy, onMount, createContext } from 'svelte';
|
||||
|
||||
/**
|
||||
* Options for initializing the TrackingManager.
|
||||
@@ -36,17 +38,69 @@ type InternalService<T> = Service<T> & {
|
||||
* Manages user tracking preferences and services that require consent.
|
||||
*/
|
||||
export class TrackingManager {
|
||||
/** tracking consent, persisted to localStorage by saveOpts */
|
||||
private _consent: boolean | null = $state(null);
|
||||
private _services: Record<string, InternalService<unknown>> = {};
|
||||
private _changeCallbacks: Array<(consent: boolean | null) => void> = [];
|
||||
private _consentQueue: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* Saves consent state to localStorage if browser storage is available.
|
||||
* Automatically called after updating consent.
|
||||
* @throws Error if storage is not available.
|
||||
*/
|
||||
saveOpts(): TrackingManager {
|
||||
if (!browser || !window?.localStorage) {
|
||||
throw new Error('Cannot access localStorage to save tracking state');
|
||||
}
|
||||
|
||||
window.localStorage.setItem(
|
||||
'trackingOpts',
|
||||
JSON.stringify({ consent: this._consent } as TrackingManagerOpts)
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads tracking options from localStorage if available. Suitable to call
|
||||
* after initialization, e.g. in onMount after a TrackingManager is created.
|
||||
* @throws Error if storage is not available.
|
||||
*/
|
||||
loadOpts(): TrackingManager {
|
||||
if (!browser || !window?.localStorage) {
|
||||
throw new Error('Cannot access localStorage to load tracking state');
|
||||
}
|
||||
|
||||
const raw = window.localStorage.getItem('trackingOpts');
|
||||
if (raw) {
|
||||
const opts = JSON.parse(raw) as TrackingManagerOpts;
|
||||
if (opts.consent !== undefined && opts.consent !== null) {
|
||||
this.setConsent(opts.consent);
|
||||
}
|
||||
log.debug('[TrackingManager] Loaded tracking options from storage:', opts);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TrackingManager instance.
|
||||
* @param opts Optional initial options.
|
||||
*/
|
||||
constructor(opts?: TrackingManagerOpts) {
|
||||
if (opts) {
|
||||
if (opts.consent !== undefined) this._consent = opts.consent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TrackingManager instance from localStorage data.
|
||||
* @throws Error if storage is not available.
|
||||
*/
|
||||
static fromLocalStorage(): TrackingManager {
|
||||
return new TrackingManager().loadOpts();
|
||||
}
|
||||
|
||||
/** Indicates whether tracking is currently allowed. */
|
||||
get consent() {
|
||||
return this._consent;
|
||||
@@ -77,10 +131,10 @@ export class TrackingManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether tracking is allowed. If set to true, all queued callbacks
|
||||
* will be executed.
|
||||
* Sets whether tracking is consented. If set to true, all queued callbacks
|
||||
* will be executed. Automatically persists to localStorage if available.
|
||||
*/
|
||||
setAllowed(value: boolean) {
|
||||
setConsent(value: boolean) {
|
||||
if (this._consent === value) return;
|
||||
this._consent = value;
|
||||
|
||||
@@ -95,6 +149,7 @@ export class TrackingManager {
|
||||
this._changeCallbacks.forEach((cb) => {
|
||||
cb(this._consent);
|
||||
});
|
||||
this.saveOpts();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,16 +168,23 @@ export class TrackingManager {
|
||||
|
||||
/**
|
||||
* Runs callback immediately if we have consent already or queues it for later.
|
||||
* Removes callback from queue onDestroy.
|
||||
* @param callback The function to run when consent is granted.
|
||||
*/
|
||||
runWithConsent(callback: () => void) {
|
||||
if (this._consent) {
|
||||
callback();
|
||||
} else {
|
||||
this._consentQueue.push(callback);
|
||||
}
|
||||
if (this._consent) callback();
|
||||
else this._consentQueue.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs callback onMount if we have consent already or queues it for later.
|
||||
* Removes the callback from the queue onDestroy.
|
||||
* @param callback The function to run when consent is granted.
|
||||
*/
|
||||
lifecycleWithConsent(callback: () => void) {
|
||||
onMount(() => {
|
||||
if (this._consent) callback();
|
||||
else this._consentQueue.push(callback);
|
||||
});
|
||||
onDestroy(() => {
|
||||
this._consentQueue = this._consentQueue.filter((cb) => cb !== callback);
|
||||
});
|
||||
@@ -192,19 +254,31 @@ export class TrackingManager {
|
||||
}
|
||||
}
|
||||
|
||||
const trackingManagerKey = Symbol();
|
||||
const [getTrackingContext, setTrackingContext] = createContext<TrackingManager>();
|
||||
|
||||
/**
|
||||
* Gets the TrackingManager from context, or creates one if it doesn't exist.
|
||||
* @param initializer Optional initializer function to customize the TrackingManager.
|
||||
* If called from the browser, attempts to load saved state from localStorage.
|
||||
* @returns The TrackingManager instance.
|
||||
*/
|
||||
export const getTrackingManager = (): TrackingManager => {
|
||||
const saved = getContext<TrackingManager>(trackingManagerKey);
|
||||
if (saved) return saved;
|
||||
try {
|
||||
const saved = getTrackingContext();
|
||||
if (saved) {
|
||||
log.debug('[TrackingManager] Using existing instance from context');
|
||||
return saved;
|
||||
}
|
||||
} catch {
|
||||
// ignore missing context, we'll create a new one
|
||||
}
|
||||
|
||||
log.debug('[TrackingManager] Creating new instance');
|
||||
const manager = $state(new TrackingManager());
|
||||
setTrackingContext(manager);
|
||||
|
||||
if (browser) {
|
||||
manager.loadOpts();
|
||||
}
|
||||
|
||||
console.debug('initializing a new TrackingManager');
|
||||
const manager = new TrackingManager();
|
||||
setContext(trackingManagerKey, manager);
|
||||
return manager;
|
||||
};
|
||||
|
||||
5
src/lib/types/fbq.d.ts
vendored
5
src/lib/types/fbq.d.ts
vendored
@@ -340,6 +340,11 @@ export interface FBQ {
|
||||
// consent and LDU
|
||||
(cmd: 'consent', state: 'grant' | 'revoke'): void;
|
||||
(cmd: 'dataProcessingOptions', options: string[], countryCode?: number, stateCode?: number): void;
|
||||
|
||||
/** Prevent automatic listening to history.pushState/popstate */
|
||||
disablePushState?: boolean;
|
||||
/** Allow duplicate page view events (legacy / undocumented behavior) */
|
||||
allowDuplicatePageViews?: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { browser } from '$app/environment';
|
||||
import log from 'loglevel';
|
||||
|
||||
const SCRIPT_SRC = 'https://connect.facebook.net/en_US/fbevents.js';
|
||||
|
||||
@@ -8,22 +9,60 @@ type QueuedFBQ = ((...args: unknown[]) => void) & {
|
||||
loaded?: boolean;
|
||||
version?: string;
|
||||
push?: unknown;
|
||||
disablePushState?: boolean;
|
||||
allowDuplicatePageViews?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the Meta Pixel script and configures the `fbq` function to queue
|
||||
* commands until the script is fully loaded. You may optionally await the
|
||||
* returned Promise to ensure the script has loaded before proceeding.
|
||||
*
|
||||
* Options:
|
||||
* - `disablePushState` (default: true) — when true, sets
|
||||
* `window.fbq.disablePushState = true` before the pixel script loads so the
|
||||
* pixel does not auto-listen to `history.pushState`/`popstate` (recommended
|
||||
* for SPA frameworks like Svelte).
|
||||
* - `allowDuplicatePageViews` (default: false) — when true, sets
|
||||
* `window.fbq.allowDuplicatePageViews = true` on the stub.
|
||||
*/
|
||||
export const loadMetaPixel = (): Promise<void> => {
|
||||
export const loadMetaPixel = (opts?: {
|
||||
disablePushState?: boolean;
|
||||
allowDuplicatePageViews?: boolean;
|
||||
}): Promise<void> => {
|
||||
// Make sure we're using the browser
|
||||
if (!browser || !window) {
|
||||
return Promise.reject(new Error('Window is undefined'));
|
||||
return Promise.reject(new Error(`Not in browser, can't access window`));
|
||||
}
|
||||
|
||||
// Default behavior: disable pushState handling since Svelte apps manage
|
||||
// navigation themselves and Meta's auto-patching of history APIs can
|
||||
// cause duplicate/incorrect pageview events. Consumers can pass
|
||||
// `opts.disablePushState = false` to opt out.
|
||||
const disablePushState = opts?.disablePushState ?? true;
|
||||
const allowDuplicatePageViews = opts?.allowDuplicatePageViews ?? false;
|
||||
|
||||
// If fbq is already defined, resolve immediately
|
||||
const existing = window.fbq as QueuedFBQ | undefined;
|
||||
if (existing && existing.loaded) {
|
||||
if (existing) {
|
||||
// If the existing stub is present but hasn't set these flags yet, set
|
||||
// them now so the loaded library (if it inspects them) sees intended
|
||||
// behavior. Setting these is a no-op if initialization already
|
||||
// completed.
|
||||
if (disablePushState) existing.disablePushState = true;
|
||||
if (allowDuplicatePageViews) existing.allowDuplicatePageViews = true;
|
||||
const existingScript = getExistingScript();
|
||||
if (existingScript) {
|
||||
return new Promise((resolve, reject) => {
|
||||
attachToScript(existingScript, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
log.debug(
|
||||
'Meta Pixel fbq already present, skipping injection',
|
||||
existing.version,
|
||||
existing.queue
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -40,18 +79,18 @@ export const loadMetaPixel = (): Promise<void> => {
|
||||
q.push = q;
|
||||
q.loaded = true;
|
||||
q.version = '2.0';
|
||||
// set control flags on the stub before the meta script runs
|
||||
if (disablePushState) q.disablePushState = true;
|
||||
if (allowDuplicatePageViews) q.allowDuplicatePageViews = true;
|
||||
window.fbq = q;
|
||||
window._fbq = q;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Avoid adding the same script twice
|
||||
const existingScript = document.querySelector(
|
||||
`script[src="${SCRIPT_SRC}"]`
|
||||
) as HTMLScriptElement | null;
|
||||
const existingScript = getExistingScript();
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener('load', () => resolve());
|
||||
existingScript.addEventListener('error', () =>
|
||||
reject(new Error('Failed to load Meta Pixel script'))
|
||||
);
|
||||
attachToScript(existingScript, resolve, reject);
|
||||
log.debug('Meta Pixel script already present, waiting for load');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,8 +98,23 @@ export const loadMetaPixel = (): Promise<void> => {
|
||||
const script = document.createElement('script');
|
||||
script.src = SCRIPT_SRC;
|
||||
script.async = true;
|
||||
script.addEventListener('load', () => resolve());
|
||||
script.addEventListener('error', () => reject(new Error('Failed to load Meta Pixel script')));
|
||||
attachToScript(script, resolve, reject);
|
||||
document.head.appendChild(script);
|
||||
log.debug('Meta Pixel script added to document');
|
||||
});
|
||||
};
|
||||
|
||||
const getExistingScript = (): HTMLScriptElement | null => {
|
||||
return document.querySelector(
|
||||
`script[src*="connect.facebook.net"][src*="fbevents.js"]`
|
||||
) as HTMLScriptElement | null;
|
||||
};
|
||||
|
||||
const attachToScript = (
|
||||
el: HTMLScriptElement,
|
||||
resolve: () => void,
|
||||
reject: (err: Error) => void
|
||||
) => {
|
||||
el.addEventListener('load', () => resolve());
|
||||
el.addEventListener('error', () => reject(new Error('Failed to load Meta Pixel script')));
|
||||
};
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
preprocess: vitePreprocess()
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user