32 Commits

Author SHA1 Message Date
Elijah Duffy
a9a9f5ed30 0.0.6 2025-12-18 22:08:31 -08:00
Elijah Duffy
0e1a449cb6 conversion: more robust fbp, fbc with header-based attempt 2025-12-18 22:06:23 -08:00
Elijah Duffy
764da5db2e conversion: make event ID optional 2025-12-18 22:04:32 -08:00
Elijah Duffy
6692338b83 pixel: allow disabling conversion API forwarding 2025-12-18 21:27:44 -08:00
Elijah Duffy
3561012fb9 pixel: fix $state usage, fix load ordering 2025-12-18 21:22:07 -08:00
Elijah Duffy
b26f6160f8 set log level to warn in production 2025-12-18 21:21:46 -08:00
Elijah Duffy
1bb202ffa5 pixel: rename pixel-control to svelte file 2025-12-18 19:24:28 -08:00
Elijah Duffy
0cd3f10da6 pixel: add ensure fbc fallback 2025-12-18 19:22:54 -08:00
Elijah Duffy
f2d389ee64 improve logging consistency 2025-12-18 19:03:27 -08:00
Elijah Duffy
f5ec7b3812 conversion: explicit event params, removes any in PixelControl 2025-12-18 18:44:33 -08:00
Elijah Duffy
fc07bb057c pixel: refactor to separate files 2025-12-18 17:20:05 -08:00
Elijah Duffy
9400e81aaa pixel: integrate conversion API 2025-12-18 17:12:57 -08:00
Elijah Duffy
824fd262ed conversion: fix exports and strengthen IP extraction 2025-12-18 17:12:47 -08:00
Elijah Duffy
82cce84a4e capi: proper dev mode handling requiring test event code 2025-12-18 13:07:39 -08:00
Elijah Duffy
2e12d281ef conversion api wrapper 2025-12-18 12:56:49 -08:00
Elijah Duffy
99c1f003c6 0.0.5 2025-12-18 10:33:25 -08:00
Elijah Duffy
97497db8e4 pixel: don't send events in dev mode without a test code 2025-12-18 10:33:17 -08:00
Elijah Duffy
674663b027 0.0.4 2025-12-18 10:23:17 -08:00
Elijah Duffy
e7b12f50b1 pixel: improve fetching existing PixelControls 2025-12-18 10:23:12 -08:00
Elijah Duffy
927b02d30e 0.0.3 2025-12-17 22:19:25 -08:00
Elijah Duffy
45a6bda53e bump required svelte version 2025-12-17 22:19:11 -08:00
Elijah Duffy
9a72280737 meta pixel: default to disablePushState = true
Breaks SvelteKit SPA which doesn't allow use of history API, requiring
its own wrapper to be used instead.
2025-12-17 22:19:05 -08:00
Elijah Duffy
095462c80d tracking manager: add localStorage persistence 2025-12-17 22:18:29 -08:00
Elijah Duffy
bb92e25485 meta pixel: more robust loading & graceful failure with adblockers 2025-12-16 21:04:32 -08:00
Elijah Duffy
eeccb09b0b 0.0.2 2025-12-16 18:24:28 -08:00
Elijah Duffy
ff99579ff1 clean up build warnings 2025-12-16 18:24:24 -08:00
Elijah Duffy
802a825854 add logging with loglevel 2025-12-16 18:21:39 -08:00
Elijah Duffy
af1c423ccb fix type exports 2025-12-16 18:21:25 -08:00
Elijah Duffy
110f0d2434 finish separating meta init from PixelControl.for 2025-12-16 18:07:59 -08:00
Elijah Duffy
51dfcfde59 tracking: rename setAllowed to setConsent 2025-12-16 17:56:06 -08:00
Elijah Duffy
ae08a564a8 separate meta init from PixelControl.for 2025-12-16 17:55:18 -08:00
Elijah Duffy
04ce2d3c57 fix component exports 2025-12-16 17:54:54 -08:00
18 changed files with 1378 additions and 252 deletions

View File

@@ -4,7 +4,8 @@
"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.1", "version": "0.0.6",
"license": "MIT",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build && npm run prepack", "build": "vite build && npm run prepack",
@@ -34,12 +35,12 @@
} }
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^5.0.0" "@sveltejs/kit": "^2.0.0",
"svelte": "^5.40.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.1", "@sveltejs/kit": "^2.49.1",
"@sveltejs/package": "^2.5.7", "@sveltejs/package": "^2.5.7",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
@@ -61,7 +62,11 @@
"svelte" "svelte"
], ],
"dependencies": { "dependencies": {
"@types/umami": "^2.10.1" "@types/facebook-nodejs-business-sdk": "^23.0.0",
"@types/umami": "^2.10.1",
"facebook-nodejs-business-sdk": "^24.0.1",
"http-status-codes": "^2.3.0",
"loglevel": "^1.9.2"
}, },
"publishConfig": { "publishConfig": {
"registry": "https://gitea.auvem.com/api/packages/svelte-toolkit/npm/" "registry": "https://gitea.auvem.com/api/packages/svelte-toolkit/npm/"

280
pnpm-lock.yaml generated
View File

@@ -8,9 +8,21 @@ importers:
.: .:
dependencies: dependencies:
'@types/facebook-nodejs-business-sdk':
specifier: ^23.0.0
version: 23.0.0
'@types/umami': '@types/umami':
specifier: ^2.10.1 specifier: ^2.10.1
version: 2.10.1 version: 2.10.1
facebook-nodejs-business-sdk:
specifier: ^24.0.1
version: 24.0.1
http-status-codes:
specifier: ^2.3.0
version: 2.3.0
loglevel:
specifier: ^1.9.2
version: 1.9.2
devDependencies: devDependencies:
'@eslint/compat': '@eslint/compat':
specifier: ^1.4.0 specifier: ^1.4.0
@@ -18,9 +30,6 @@ importers:
'@eslint/js': '@eslint/js':
specifier: ^9.39.1 specifier: ^9.39.1
version: 9.39.2 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': '@sveltejs/kit':
specifier: ^2.49.1 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)) 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 +441,6 @@ packages:
peerDependencies: peerDependencies:
acorn: ^8.9.0 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': '@sveltejs/kit@2.49.2':
resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==} resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==}
engines: {node: '>=18.13'} engines: {node: '>=18.13'}
@@ -478,6 +482,9 @@ packages:
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/facebook-nodejs-business-sdk@23.0.0':
resolution: {integrity: sha512-k9saLiTd4kvBCdBHEpTyDMYlqSsiDw0yHs2ePe5M1PTIldtdtNOGx73Gtu8dQqbTAlwBWbiG3VfI+9s45Cf88w==}
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -570,6 +577,12 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
axobject-query@4.1.0: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -583,6 +596,10 @@ packages:
brace-expansion@2.0.2: brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
callsites@3.1.0: callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -610,6 +627,10 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -626,6 +647,9 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
hasBin: true hasBin: true
currency-codes@1.5.1:
resolution: {integrity: sha512-hqy8vtlIYKzO6pe2TE0V4/riZALIc7nhtE9cvxk5FDRCvfGplgzUvpTmZlMsyO+NeK5U41j+sQXJOo8l8v9kdg==}
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -645,9 +669,37 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
devalue@5.6.1: devalue@5.6.1:
resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
email-validator@2.0.4:
resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==}
engines: {node: '>4.0'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.1: esbuild@0.27.1:
resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -721,6 +773,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
facebook-nodejs-business-sdk@24.0.1:
resolution: {integrity: sha512-31uR42FO+V3ikQfVZyrnoc34pVYzxo4PE+BnKt1ziX9nGhSwsS7ogGDsMJQk7A3SdoWPIGgR2G4UbAY2s9zNVQ==}
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -747,6 +802,9 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
first-match@0.0.1:
resolution: {integrity: sha512-VvKbnaxrC0polTFDC+teKPTdl2mn6B/KUW+WB3C9RzKDeNwbzfLdnUz3FxC+tnjvus6bI0jWrWicQyVIPdS37A==}
flat-cache@4.0.1: flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -754,11 +812,35 @@ packages:
flatted@3.3.3: flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
glob-parent@6.0.2: glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -771,10 +853,29 @@ packages:
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-flag@4.0.0: has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
http-status-codes@2.3.0:
resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==}
ignore@5.3.2: ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@@ -805,6 +906,12 @@ packages:
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
iso-3166-1@2.1.1:
resolution: {integrity: sha512-RZxXf8cw5Y8LyHZIwIRvKw8sWTIHh2/txBT+ehO0QroesVfnz3JNFFX4i/OC/Yuv2bDIVYrHna5PMvjtpefq5w==}
js-sha256@0.9.0:
resolution: {integrity: sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==}
js-yaml@4.1.1: js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true hasBin: true
@@ -846,9 +953,25 @@ packages:
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 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: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@3.1.2: minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -856,6 +979,9 @@ packages:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
mixwith@0.1.1:
resolution: {integrity: sha512-DQsf/liljH/9e+94jR+xfK8vlKceeKdOM9H9UEXLwGuvEEpO6debNtJ9yt1ZKzPKPrwqGxzMdu0BR1fnQb6i4A==}
mri@1.2.0: mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -875,6 +1001,9 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
nub@0.0.0:
resolution: {integrity: sha512-dK0Ss9C34R/vV0FfYJXuqDAqHlaW9fvWVufq9MmGF2umCuDbd5GRfRD9fpi/LiM0l4ZXf8IBB+RYmZExqCrf0w==}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -956,6 +1085,9 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
publint@0.3.16: publint@0.3.16:
resolution: {integrity: sha512-MFqyfRLAExPVZdTQFwkAQELzA8idyXzROVOytg6nEJ/GEypXBUmMGrVaID8cTuzRS1U5L8yTOdOJtMXgFUJAeA==} resolution: {integrity: sha512-MFqyfRLAExPVZdTQFwkAQELzA8idyXzROVOytg6nEJ/GEypXBUmMGrVaID8cTuzRS1U5L8yTOdOJtMXgFUJAeA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1393,10 +1525,6 @@ snapshots:
dependencies: dependencies:
acorn: 8.15.0 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))': '@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: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
@@ -1452,6 +1580,8 @@ snapshots:
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/facebook-nodejs-business-sdk@23.0.0': {}
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/node@24.10.4': '@types/node@24.10.4':
@@ -1572,6 +1702,16 @@ snapshots:
aria-query@5.3.2: {} aria-query@5.3.2: {}
asynckit@0.4.0: {}
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
@@ -1585,6 +1725,11 @@ snapshots:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
callsites@3.1.0: {} callsites@3.1.0: {}
chalk@4.1.2: chalk@4.1.2:
@@ -1608,6 +1753,10 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
concat-map@0.0.1: {} concat-map@0.0.1: {}
cookie@0.6.0: {} cookie@0.6.0: {}
@@ -1620,6 +1769,11 @@ snapshots:
cssesc@3.0.0: {} cssesc@3.0.0: {}
currency-codes@1.5.1:
dependencies:
first-match: 0.0.1
nub: 0.0.0
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@@ -1630,8 +1784,33 @@ snapshots:
deepmerge@4.3.1: {} deepmerge@4.3.1: {}
delayed-stream@1.0.0: {}
devalue@5.6.1: {} devalue@5.6.1: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
email-validator@2.0.4: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.27.1: esbuild@0.27.1:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.27.1 '@esbuild/aix-ppc64': 0.27.1
@@ -1757,6 +1936,17 @@ snapshots:
esutils@2.0.3: {} esutils@2.0.3: {}
facebook-nodejs-business-sdk@24.0.1:
dependencies:
axios: 1.13.2
currency-codes: 1.5.1
email-validator: 2.0.4
iso-3166-1: 2.1.1
js-sha256: 0.9.0
mixwith: 0.1.1
transitivePeerDependencies:
- debug
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-json-stable-stringify@2.1.0: {} fast-json-stable-stringify@2.1.0: {}
@@ -1776,6 +1966,8 @@ snapshots:
locate-path: 6.0.0 locate-path: 6.0.0
path-exists: 4.0.0 path-exists: 4.0.0
first-match@0.0.1: {}
flat-cache@4.0.1: flat-cache@4.0.1:
dependencies: dependencies:
flatted: 3.3.3 flatted: 3.3.3
@@ -1783,9 +1975,39 @@ snapshots:
flatted@3.3.3: {} flatted@3.3.3: {}
follow-redirects@1.15.11: {}
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
function-bind@1.1.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
glob-parent@6.0.2: glob-parent@6.0.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -1794,8 +2016,22 @@ snapshots:
globals@16.5.0: {} globals@16.5.0: {}
gopd@1.2.0: {}
has-flag@4.0.0: {} has-flag@4.0.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
http-status-codes@2.3.0: {}
ignore@5.3.2: {} ignore@5.3.2: {}
ignore@7.0.5: {} ignore@7.0.5: {}
@@ -1819,6 +2055,10 @@ snapshots:
isexe@2.0.0: {} isexe@2.0.0: {}
iso-3166-1@2.1.1: {}
js-sha256@0.9.0: {}
js-yaml@4.1.1: js-yaml@4.1.1:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
@@ -1852,10 +2092,20 @@ snapshots:
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
loglevel@1.9.2: {}
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12
@@ -1864,6 +2114,8 @@ snapshots:
dependencies: dependencies:
brace-expansion: 2.0.2 brace-expansion: 2.0.2
mixwith@0.1.1: {}
mri@1.2.0: {} mri@1.2.0: {}
mrmime@2.0.1: {} mrmime@2.0.1: {}
@@ -1874,6 +2126,8 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
nub@0.0.0: {}
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@@ -1940,6 +2194,8 @@ snapshots:
prettier@3.7.4: {} prettier@3.7.4: {}
proxy-from-env@1.1.0: {}
publint@0.3.16: publint@0.3.16:
dependencies: dependencies:
'@publint/pack': 0.1.2 '@publint/pack': 0.1.2

View File

@@ -1,190 +0,0 @@
<script lang="ts" module>
export class PixelControl {
private _pixelID: string;
private _testEventCode?: string = undefined;
private _trackingManager: MaybeGetter<TrackingManager | undefined>;
private static _baseLoaded: boolean = false;
static get baseLoaded(): boolean {
return this._baseLoaded;
}
/**
* Ensures that the Meta Pixel base has been loaded before
* allowing further operations.
* @throws Error if the Meta Pixel API is not loaded.
*/
static loadGuard(): void {
if (!this._baseLoaded || !window._fbq) {
throw new Error('Meta Pixel API has not been loaded. Call PixelControl.load() first.');
}
}
private constructor(
trackingManager: MaybeGetter<TrackingManager | undefined>,
pixelID: string,
testEventCode?: string
) {
this._trackingManager = trackingManager;
this._pixelID = pixelID;
this._testEventCode = 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
}
this._baseLoaded = true;
}
/** Tells the Meta pixel that the user has given consent for tracking. */
static grantConsent() {
this.loadGuard();
window.fbq?.('consent', 'grant');
}
/** Tells the Meta pixel that the user has revoked consent for tracking. */
static revokeConsent() {
this.loadGuard();
window.fbq?.('consent', 'revoke');
}
/**
* Returns 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.
* @param pixelID Meta Pixel ID
* @param testEventCode Optional test event code
* @returns PixelControl instance
*/
static for(
trackingManager: MaybeGetter<TrackingManager | undefined>,
pixelID: string,
options?: {
testEventCode?: string;
advancedMatching?: AdvancedMatching;
initOptions?: InitOptions;
}
): PixelControl {
PixelControl.load();
window.fbq('init', pixelID);
return new PixelControl(trackingManager, pixelID, options?.testEventCode);
}
/**
* Checks if the Meta Pixel has consent to track user data
* and if the Pixel has been loaded.
* @returns true if tracking is allowed, false otherwise.
* @throws Error if the Meta Pixel is not loaded.
*/
consentGuard(): boolean {
PixelControl.loadGuard();
const trackingManager = resolveGetter(this._trackingManager);
return trackingManager?.haveUserConsent() ?? false;
}
/**
* Sends a PageView event
* @throws Error if the Meta Pixel is not initialized.
*/
pageView() {
if (!this.consentGuard()) return;
window.fbq('track', 'PageView', undefined, { test_event_code: this._testEventCode });
}
/**
* Tracks a standard event for this pixel (uses `trackSingle` under the hood)
* @throws Error if the Meta Pixel is not initialized.
*/
track<K extends StandardEventName>(event: K, params?: EventParamsByName[K], eventID?: string) {
if (!this.consentGuard()) return;
window.fbq('trackSingle', this._pixelID, event, params, {
eventID,
test_event_code: this._testEventCode
});
}
/**
* Tracks a custom event for this pixel (uses `trackSingleCustom` under the hood)
* @throws Error if the Meta Pixel is not initialized.
*/
trackCustom(event: string, params?: CommonParams & CustomParams, eventID?: string) {
if (!this.consentGuard()) return;
window.fbq('trackSingleCustom', this._pixelID, event, params, {
eventID,
test_event_code: this._testEventCode
});
}
}
</script>
<script lang="ts">
import { onMount } from 'svelte';
import type { TrackingManager } from './tracking.svelte.ts';
import type {
EventParamsByName,
StandardEventName,
CommonParams,
CustomParams,
AdvancedMatching,
InitOptions
} from './types/fbq.js';
import { loadMetaPixel } from './util/meta-pixel-loader.ts';
import { onNavigate } from '$app/navigation';
import { resolveGetter, type MaybeGetter } from './util/getter.ts';
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;
}
let { pixelID, testEventCode, autoPageView = true, trackingManager }: Props = $props();
let pixel = $state<PixelControl | null>(null);
onMount(() => {
if (!trackingManager) {
throw new Error('MetaPixel component requires a TrackingManager to manage consent.');
}
PixelControl.load();
pixel = PixelControl.for(trackingManager, pixelID, { testEventCode });
trackingManager.runWithConsent(() => {
if (autoPageView && pixel) {
pixel.pageView();
}
});
});
onNavigate(() => {
trackingManager?.runWithConsent(() => {
if (autoPageView && pixel) {
pixel.pageView();
}
});
});
</script>

View File

@@ -2,6 +2,7 @@
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { TrackingManager } from './tracking.svelte.ts'; import type { TrackingManager } from './tracking.svelte.ts';
import log from 'loglevel';
interface Props { interface Props {
/** /**
@@ -24,14 +25,16 @@
const consentGranted = $derived(trackingManager ? trackingManager.consent === true : true); const consentGranted = $derived(trackingManager ? trackingManager.consent === true : true);
// Development overrides to prevent dirty analytics // Development overrides to prevent dirty analytics
const devConsoleTag = $derived(`[dev][consent: ${consentGranted ? 'granted' : 'revoked'}]`); const devConsoleTag = $derived(
`[Umami] [dev][consent: ${consentGranted ? 'granted' : 'revoked'}]`
);
const devOverride = { const devOverride = {
track: (...args: unknown[]): Promise<string> | undefined => { track: (...args: unknown[]): Promise<string> | undefined => {
console.log(`${devConsoleTag}: Track called with:`, ...args); log.debug(`${devConsoleTag}: Track called with:`, ...args);
return undefined; return undefined;
}, },
identify: (...args: unknown[]): Promise<void> => { identify: (...args: unknown[]): Promise<void> => {
console.log(`${devConsoleTag}: Identify called with:`, ...args); log.debug(`${devConsoleTag}: Identify called with:`, ...args);
return Promise.resolve(); return Promise.resolve();
} }
}; };
@@ -61,7 +64,7 @@
} }
}); });
if (dev) console.log('[dev]: Umami tracking disabled'); if (dev) log.info('[Umami] [dev]: tracking disabled');
onMount(() => { onMount(() => {
if (dev) { if (dev) {

View File

@@ -0,0 +1,92 @@
import { getFbpFbc } from '../metapixel/fbc.ts';
import type { TrackingManager } from '$lib/tracking.svelte';
import type {
ConversionErrorResponseBody,
ConversionEventParams,
ConversionRequestBody,
ConversionResponseBody,
ConversionUserData
} from '$lib/types/conversion.js';
import type { StandardEventName } from '$lib/types/fbq.js';
/**
* Client for sending conversion events to a server endpoint.
*/
export class ConversionClient {
private _href: string;
private _trackingManager: TrackingManager;
/**
* Creates a new ConversionClient.
*
* @param serverHref - The server endpoint URL.
* @param trackingManager - The tracking manager instance.
*/
constructor(serverHref: string, trackingManager: TrackingManager) {
this._href = serverHref;
this._trackingManager = trackingManager;
}
/**
* Sends a conversion event to the server.
*
* @param eventName - The name of the standard event to send.
* @param options - Additional options for the event.
* @returns A promise that resolves to the event response or error.
*/
async trackEvent(
eventName: StandardEventName,
options: {
eventID: string;
user?: Omit<ConversionUserData, 'ip' | 'fbp' | 'fbc' | 'ua'>;
customData?: ConversionEventParams;
}
): Promise<ConversionResponseBody | ConversionErrorResponseBody> {
// Extract user data
const { fbp, fbc } = getFbpFbc();
const user: ConversionUserData = {
...options.user,
fbp,
fbc
};
// Get event source URL & extract UTM params if present
const eventSourceURL = window.location.href;
const url = new URL(eventSourceURL);
const utms: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
if (key.startsWith('utm_')) {
utms[key] = value;
}
});
// Build request body
const requestBody: ConversionRequestBody = {
consent: this._trackingManager.consent === true,
eventName,
eventID: options.eventID,
user,
eventSourceURL,
utms,
customData: options.customData
};
// Send request to server
const response = await fetch(this._href, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const data = (await response.json()) as ConversionResponseBody;
return data;
} else {
const errorData = (await response.json()) as ConversionErrorResponseBody;
return errorData;
}
}
}

View File

@@ -0,0 +1,157 @@
import {
Content,
CustomData,
EventRequest,
ServerEvent,
UserData
} from 'facebook-nodejs-business-sdk';
import type { StandardEventName } from '../types/fbq.js';
import type {
ConversionEventDetails,
ConversionEventParams,
ConversionResponseBody,
ConversionUserData
} from '$lib/types/conversion.js';
import { dev } from '$app/environment';
import log from 'loglevel';
/**
* Builds UserData for conversion events.
*
* @param data - The user data to include.
* @returns The constructed UserData object.
*/
const buildUserData = (data: ConversionUserData): UserData => {
const userData = new UserData();
if (data.email) userData.setEmail(data.email);
if (data.phone) userData.setPhone(data.phone);
if (data.firstName) userData.setFirstName(data.firstName);
if (data.lastName) userData.setLastName(data.lastName);
if (data.ip) userData.setClientIpAddress(data.ip);
if (data.fbp) userData.setFbp(data.fbp);
if (data.fbc) userData.setFbc(data.fbc);
if (data.ua) userData.setClientUserAgent(data.ua);
if (data.externalId) userData.setExternalId(data.externalId);
return userData;
};
/**
* Builds CustomData for conversion events.
*
* @param params - The custom data parameters.
* @returns The constructed CustomData object.
*/
const buildCustomData = (params: ConversionEventParams): CustomData => {
const c = new CustomData();
if (params.value) c.setValue(params.value);
if (params.net_revenue) c.setNetRevenue(params.net_revenue);
if (params.currency) c.setCurrency(params.currency);
if (params.content_name) c.setContentName(params.content_name);
if (params.content_category) c.setContentCategory(params.content_category);
if (params.content_ids) c.setContentIds(params.content_ids);
if (params.contents) {
const contents: Content[] = params.contents.map((content) => {
const c = new Content();
if (content.id) c.setId(content.id.toString());
if (content.quantity) c.setQuantity(content.quantity);
if (content.item_price) c.setItemPrice(content.item_price);
if (content.title) c.setTitle(content.title);
if (content.description) c.setDescription(content.description);
if (content.category) c.setCategory(content.category);
if (content.brand) c.setBrand(content.brand);
if (content.delivery_category) c.setDeliveryCategory(content.delivery_category);
return c;
});
c.setContents(contents);
}
if (params.content_type) c.setContentType(params.content_type);
if (params.order_id) c.setOrderId(params.order_id);
if (params.predicted_ltv) c.setPredictedLtv(params.predicted_ltv);
if (params.num_items) c.setNumItems(params.num_items);
if (params.search_string) c.setSearchString(params.search_string);
if (params.status) c.setStatus(params.status);
if (params.item_number) c.setItemNumber(params.item_number);
if (params.delivery_category) c.setDeliveryCategory(params.delivery_category);
if (params.custom_properties) c.setCustomProperties(params.custom_properties);
return c;
};
/**
* Control class for sending Conversion API events to Meta Pixel.
*/
export class ConversionControl {
private _accessToken: string;
private _pixelID: string;
private _testEventCode?: string;
/**
* Creates a new ConversionControl instance.
*
* @param accessToken - Your Meta Pixel Conversion API access token.
* @param pixelID - Your Meta Pixel ID.
* @param testEventCode - Optional test event code for testing events.
*/
constructor(accessToken: string, pixelID: string, testEventCode?: string) {
this._accessToken = accessToken;
this._pixelID = pixelID;
this._testEventCode = testEventCode;
}
/**
* Sends a conversion event to Meta Pixel. Requires a test event code for
* events to be sent to Meta when in development mode.
*
* @param eventName - The name of the standard event to send.
* @param options - Additional options for the event.
* @returns A promise that resolves to the event response.
*/
async trackEvent(
eventName: StandardEventName,
details: ConversionEventDetails,
params?: ConversionEventParams
): Promise<ConversionResponseBody> {
const event = new ServerEvent()
.setEventName(eventName)
.setEventTime(Math.floor(Date.now() / 1000))
.setUserData(buildUserData(details.userData))
.setActionSource(details.actionSource);
if (details.eventID) event.setEventId(details.eventID);
if (details.eventSourceURL) event.setEventSourceUrl(details.eventSourceURL);
if (params) {
const customData = buildCustomData(params);
event.setCustomData(customData);
}
// If we're in dev mode and missing a test event code, log and exit
if (dev && !this._testEventCode) {
log.debug(
`[ConversionControl] ${eventName} event not sent - missing test event code in dev mode.`
);
return Promise.resolve({
pixelID: this._pixelID,
fbtrace_id: 'dev-mode-no-test-event-code',
receivedEvents: 1,
processedEvents: 0,
messages: ['Event not sent - missing test event code in dev mode.']
});
}
const req = new EventRequest(this._accessToken, this._pixelID).setEvents([event]);
if (this._testEventCode) req.setTestEventCode(this._testEventCode);
const response = await req.execute();
const structuredResponse: ConversionResponseBody = {
pixelID: response.id,
fbtrace_id: response.fbtrace_id,
receivedEvents: response.events_received,
processedEvents: response.num_processed_entries,
messages: response.messages
};
log.debug(
`[ConversionControl] Sent ${eventName}, Event response: ${JSON.stringify(structuredResponse)}`
);
return structuredResponse;
}
}

View File

@@ -0,0 +1,3 @@
export * from './client.ts';
export * from './server.ts';
export * from './control.ts';

View File

@@ -0,0 +1,78 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { ConversionControl } from './control.ts';
import type {
ConversionErrorResponseBody,
ConversionEventParams,
ConversionRequestBody,
ConversionUserData
} from '$lib/types/conversion.js';
import { StatusCodes } from 'http-status-codes';
import { getFbpFbcFromCookies } from '../metapixel/fbc.ts';
export const getRequestIP = (request: Request, getClientAddress: () => string) => {
return (
request.headers.get('x-forwarded-for') ||
request.headers.get('cf-connecting-ip') ||
request.headers.get('x-real-ip') ||
request.headers.get('x-client-ip') ||
request.headers.get('x-cluster-client-ip') ||
request.headers.get('x-original-forwarded-for') ||
request.headers.get('forwarded-for') ||
request.headers.get('forwarded') ||
getClientAddress()
);
};
export const createConversionRequestHandler: (control: ConversionControl) => RequestHandler = (
control
) => {
const handle: RequestHandler = async ({ request, getClientAddress, cookies }) => {
try {
const body = (await request.json()) as ConversionRequestBody;
// Build user data with IP and user agent
const ip = getRequestIP(request, getClientAddress);
const ua = request.headers.get('user-agent');
const { fbp, fbc } = getFbpFbcFromCookies(cookies);
const userData: ConversionUserData = {
...body.user,
ip,
ua: ua ?? body.user?.ua ?? undefined,
fbc: body.user?.fbc ?? fbc,
fbp: body.user?.fbp ?? fbp
};
// Build custom data with UTM params if applicable
let customData: ConversionEventParams = body.customData ?? {};
if (body.eventName === 'PageView' && body.utms) {
// For PageView events, automatically include UTM params if provided
customData = {
...customData,
...body.utms
};
}
// Send the event via the control
const response = await control.trackEvent(
body.eventName,
{
eventID: body.eventID,
eventSourceURL: body.eventSourceURL,
actionSource: 'website',
userData
},
customData
);
return json(response, { status: StatusCodes.OK });
} catch (e) {
return json(
{ error: e instanceof Error ? e.message : String(e) } as ConversionErrorResponseBody,
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
);
}
};
return handle;
};

View File

@@ -1,6 +1,18 @@
// Reexport your entry components here // Reexport your entry components here
export * as fbq from './types/fbq.js'; import { dev } from '$app/environment';
export * from './MetaPixel.svelte'; import log from 'loglevel';
export type * from './types/conversion.d.ts';
export type * as fbq from './types/fbq.d.ts';
export * from './metapixel/index.ts';
export * from './tracking.svelte.ts'; export * from './tracking.svelte.ts';
export * from './Umami.svelte'; export { default as Umami } from './Umami.svelte';
export * from './conversion/index.ts';
// set log level to debug if we're in dev mode
if (dev) {
log.setLevel('debug');
} else {
log.setLevel('warn');
}

View File

@@ -0,0 +1,74 @@
<!-- @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.
-->
<script lang="ts">
import { onMount } from 'svelte';
import type { TrackingManager } from '../tracking.svelte.ts';
import { onNavigate } from '$app/navigation';
import { PixelControl, type PixelControlOptions } from './pixel-control.ts';
import { ensureFbc, type EnsureFbcOptions } from './fbc.ts';
interface Props {
/**
* 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, pixelOptions, autoPageView = true, trackingManager }: Props = $props();
let pixel = $state<PixelControl | null>(null);
const fbcOptions: EnsureFbcOptions = {
sameSite: 'Lax'
};
onMount(() => {
if (!trackingManager) {
throw new Error('MetaPixel component requires a TrackingManager to manage consent.');
}
pixel = PixelControl.initialize(trackingManager, pixelID, pixelOptions);
trackingManager.runWithConsent(() => {
if (autoPageView && pixel) {
pixel.pageView();
ensureFbc(fbcOptions);
}
});
});
onNavigate(() => {
trackingManager?.runWithConsent(() => {
if (autoPageView && pixel) {
pixel.pageView();
ensureFbc(fbcOptions);
}
});
});
export const getPixelControl = (): PixelControl => {
if (!pixel) {
throw new Error('MetaPixel component has not been initialized yet, wait for onMount.');
}
return pixel;
};
</script>

98
src/lib/metapixel/fbc.ts Normal file
View File

@@ -0,0 +1,98 @@
import type { Cookies } from '@sveltejs/kit';
import log from 'loglevel';
export type EnsureFbcOptions = {
days?: number; // cookie lifetime, default 180
domain?: string; // optional cookie domain
sameSite?: 'Lax' | 'Strict' | 'None'; // default Lax
secure?: boolean; // default inferred from location.protocol === 'https:'
pixelLoaded?: boolean; // if true, skip manual set and let Pixel handle it
};
function getParam(name: string): string | undefined {
const params = new URLSearchParams(location.search);
const v = params.get(name);
return v || undefined;
}
function getCookie(name: string): string | undefined {
return document.cookie
.split('; ')
.find((c) => c.startsWith(name + '='))
?.split('=')[1];
}
function setCookie(
name: string,
value: string,
{
days = 180,
domain,
sameSite = 'Lax',
secure
}: Pick<EnsureFbcOptions, 'days' | 'domain' | 'sameSite' | 'secure'> = {}
) {
const d = new Date();
d.setTime(d.getTime() + days * 864e5);
const parts = [
`${name}=${encodeURIComponent(value)}`,
`expires=${d.toUTCString()}`,
'path=/',
`SameSite=${sameSite}`
];
if (domain) parts.push(`domain=${domain}`);
const isSecure = secure ?? location.protocol === 'https:';
if (isSecure) parts.push('Secure');
document.cookie = parts.join('; ');
}
function isValidFbc(value: string | undefined): boolean {
if (!value) return false;
// Expect "fb.1.<unix>.<fbclid>"
if (!value.startsWith('fb.1.')) return false;
const parts = value.split('.');
return parts.length >= 4 && /^\d+$/.test(parts[2]) && parts[3].length > 0;
}
/**
* Ensure _fbc cookie exists when landing URL contains fbclid.
* Call after consent. If pixelLoaded is true, it skips manual setting.
*/
export function ensureFbc(options: EnsureFbcOptions = {}) {
try {
const { pixelLoaded = false, ...cookieOpts } = options;
if (pixelLoaded) throw new Error('Pixel loaded, skipping manual _fbc set'); // Let the Pixel set _fbc if its active
const fbclid = getParam('fbclid');
if (!fbclid) throw new Error('No fbclid param present');
const existing = getCookie('_fbc');
if (isValidFbc(existing)) throw new Error('_fbc cookie already present and valid');
const ts = Math.floor(Date.now() / 1000);
const fbc = `fb.1.${ts}.${fbclid}`;
setCookie('_fbc', fbc, cookieOpts);
log.debug('[ensureFbc] Set _fbc cookie:', fbc);
} catch (e) {
log.debug('[ensureFbc]', (e as Error).message);
}
}
/**
* Helper to read both _fbp and _fbc for your CAPI payload from browser cookies.
*/
export function getFbpFbc(): { fbp?: string; fbc?: string } {
const fbp = getCookie('_fbp');
const fbc = getCookie('_fbc');
return { fbp, fbc };
}
/**
* Helper to read both _fbp and _fbc for your CAPI payload from cookies object.
*/
export function getFbpFbcFromCookies(cookies: Cookies): { fbp?: string; fbc?: string } {
const fbp = cookies.get('_fbp') || undefined;
const fbc = cookies.get('_fbc') || undefined;
return { fbp, fbc };
}

View File

@@ -0,0 +1,4 @@
export { default as MetaPixel } from './MetaPixel.svelte';
export { PixelControl, type PixelControlOptions } from './pixel-control.ts';
export * from './fbc.ts';
export * from './pixel-control.ts';

View File

@@ -0,0 +1,315 @@
import type { TrackingManager } from '../tracking.svelte.ts';
import type {
EventParamsByName,
StandardEventName,
CommonParams,
CustomParams,
AdvancedMatching,
InitOptions
} from '../types/fbq.js';
import { loadMetaPixel } from '../util/meta-pixel-loader.ts';
import { resolveGetter, type MaybeGetter } from '../util/getter.ts';
import log from 'loglevel';
import { dev } from '$app/environment';
import { ConversionClient } from '../conversion/client.ts';
import type { ConversionEventParams } from '$lib/types/conversion.js';
const pixelParamsToCustomData = (params: CommonParams & CustomParams): ConversionEventParams => {
const customData: ConversionEventParams = {};
if (params.value) customData.value = params.value;
if (params.currency) customData.currency = params.currency;
if (params.content_name) customData.content_name = params.content_name;
if (params.content_category) customData.content_category = params.content_category;
if (params.content_ids) customData.content_ids = params.content_ids;
if (params.contents) {
const acc: ConversionEventParams['contents'] = [];
customData.contents = params.contents.reduce((acc, content) => {
acc.push({
id: content.id.toString(),
quantity: content.quantity
});
return acc;
}, acc);
}
if (params.num_items) customData.num_items = params.num_items;
if (params.search_string) customData.search_string = params.search_string;
if (params.predicted_ltv) customData.predicted_ltv = params.predicted_ltv;
return customData;
};
/**
* Options for configuring a PixelControl instance.
*/
export type PixelControlOptions = {
/**
* if provided, events fired will always have this code attached
* to prevent them from polluting real analytics data.
*/
testEventCode?: string;
/**
* if provided, all events fired will be passed to the server endpoint
* at this URL to be sent via the Conversion API. Any events sent
* without a event ID will be assigned a random one.
*/
conversionHref?: string;
/** Advanced matching data */
advancedMatching?: AdvancedMatching;
/** Initialization options */
initOptions?: InitOptions;
};
/**
* Manages multiple Meta Pixel instances and provides methods to
* interact with them, including consent management and event tracking.
*/
export class PixelControl {
private _pixelID: string;
private _testEventCode?: string = undefined;
private _trackingManager: MaybeGetter<TrackingManager | undefined>;
private _conversionClient?: ConversionClient = 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;
}
/**
* Ensures that the Meta Pixel base has been loaded before
* allowing further operations.
* @throws Error if the Meta Pixel API is not loaded.
*/
static loadGuard(): void {
if (!this._baseLoaded || !window.fbq) {
throw new Error('Meta Pixel API has not been loaded. Call PixelControl.load() first.');
}
}
private constructor(
trackingManager: MaybeGetter<TrackingManager | undefined>,
pixelID: string,
options?: PixelControlOptions
) {
this._trackingManager = trackingManager;
this._pixelID = pixelID;
this._testEventCode = options?.testEventCode;
const resolvedTrackingManager = resolveGetter(trackingManager);
if (options?.conversionHref && resolvedTrackingManager) {
this._conversionClient = new ConversionClient(
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.`
);
}
}
/** Loads the Meta Pixel base script. */
static load() {
if (this._baseLoaded && !!window.fbq) return;
loadMetaPixel(); // Load the Meta Pixel script
this._baseLoaded = true;
log.debug('[PixelControl] Meta Pixel base script loaded.', this._baseLoaded);
}
/** Tells the Meta pixel that the user has given consent for tracking. */
static grantConsent() {
this.loadGuard();
window.fbq?.('consent', 'grant');
log.debug('[PixelControl] 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('[PixelControl] Pixel consent revoked.');
}
/**
* 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 options Optional settings
* @returns PixelControl instance
*/
static initialize(
trackingManager: MaybeGetter<TrackingManager | undefined>,
pixelID: string,
options?: PixelControlOptions
): PixelControl {
// Load the base script if not already loaded
PixelControl.load();
// 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(`[PixelControl] [${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;
}
/**
* Checks if the Meta Pixel has consent to track user data
* and if the Pixel has been loaded.
* @returns true if tracking is allowed, false otherwise.
* @throws Error if the Meta Pixel is not loaded.
*/
consentGuard(): boolean {
PixelControl.loadGuard();
const trackingManager = resolveGetter(this._trackingManager);
return trackingManager?.haveUserConsent() ?? false;
}
/**
* Sends a PageView event
* @param disableCAPI If true, disables sending this event to the Conversion API
* @throws Error if the Meta Pixel is not initialized.
*/
pageView(disableCAPI: boolean = false) {
if (!this.consentGuard()) return;
let eventID: string | undefined = undefined;
// Optionally, send to conversion API endpoint if configured
if (this._conversionClient && !disableCAPI) {
eventID = crypto.randomUUID();
this._conversionClient
.trackEvent('PageView', { eventID })
.then((response) => {
log.debug(
`[PixelControl] [${this._pixelID}] PageView event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
response
)}`
);
})
.catch((error) => {
log.error(
`[PixelControl] [${this._pixelID}] Failed to send PageView event to Conversion API with Event ID: ${eventID}`,
error
);
});
}
// Send the PageView event to Meta
if (!dev || this._testEventCode) {
window.fbq('track', 'PageView', undefined, {
test_event_code: this._testEventCode,
eventID
});
log.debug(
`[PixelControl] [${this._pixelID}] PageView event sent${dev && ` (test code: ${this._testEventCode})`}.`
);
} else {
log.info(
`[PixelControl] [${this._pixelID}] PageView event not sent in development mode without a test event code.`
);
}
}
/**
* Tracks a standard event for this pixel (uses `trackSingle` under the hood)
* @param event Standard event name
* @param params Event parameters
* @param eventID Optional event ID for deduplication with Conversion API
* @param disableCAPI If true, disables sending this event to the Conversion API
* @throws Error if the Meta Pixel is not initialized.
*/
track<K extends StandardEventName>(
event: K,
params?: EventParamsByName[K],
eventID?: string,
disableCAPI: boolean = false
) {
if (!this.consentGuard()) return;
// Optionally, send to conversion API endpoint if configured
if (this._conversionClient && !disableCAPI) {
eventID = eventID ?? crypto.randomUUID();
this._conversionClient
.trackEvent(event, { eventID: eventID, customData: pixelParamsToCustomData(params ?? {}) })
.then((response) => {
log.debug(
`[PixelControl] [${this._pixelID}] ${event} event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
response
)}`
);
})
.catch((error) => {
log.error(
`[PixelControl] [${this._pixelID}] Failed to send ${event} event to Conversion API with Event ID: ${eventID}`,
error
);
});
}
// Send the PageView event to Meta
if (!dev || this._testEventCode) {
window.fbq('trackSingle', this._pixelID, event, params, {
eventID,
test_event_code: this._testEventCode
});
log.debug(
`[PixelControl] [${this._pixelID}] ${event} event sent${dev && ` (test code: ${this._testEventCode})`}.`
);
} else {
log.info(
`[PixelControl] [${this._pixelID}] ${event} event not sent in development mode without a test event code.`
);
}
}
/**
* Tracks a custom event for this pixel (uses `trackSingleCustom` under the hood)
* @throws Error if the Meta Pixel is not initialized.
*/
trackCustom(event: string, params?: CommonParams & CustomParams, eventID?: string) {
if (!this.consentGuard()) return;
if (!dev || this._testEventCode) {
window.fbq('trackSingleCustom', this._pixelID, event, params, {
eventID,
test_event_code: this._testEventCode
});
log.debug(
`[PixelControl] [${this._pixelID}] ${event} custom event sent (test code: ${this._testEventCode}).`
);
} else {
log.info(
`[PixelControl] [${this._pixelID}] ${event} custom event not sent in development mode without a test event code.`
);
}
}
}

View File

@@ -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. * Options for initializing the TrackingManager.
@@ -36,17 +38,69 @@ type InternalService<T> = Service<T> & {
* Manages user tracking preferences and services that require consent. * Manages user tracking preferences and services that require consent.
*/ */
export class TrackingManager { export class TrackingManager {
/** tracking consent, persisted to localStorage by saveOpts */
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 _consentQueue: Array<() => 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) { constructor(opts?: TrackingManagerOpts) {
if (opts) { if (opts) {
if (opts.consent !== undefined) this._consent = opts.consent; 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. */ /** Indicates whether tracking is currently allowed. */
get consent() { get consent() {
return this._consent; return this._consent;
@@ -77,10 +131,10 @@ export class TrackingManager {
} }
/** /**
* Sets whether tracking is allowed. If set to true, all queued callbacks * Sets whether tracking is consented. If set to true, all queued callbacks
* will be executed. * will be executed. Automatically persists to localStorage if available.
*/ */
setAllowed(value: boolean) { setConsent(value: boolean) {
if (this._consent === value) return; if (this._consent === value) return;
this._consent = value; this._consent = value;
@@ -95,6 +149,7 @@ export class TrackingManager {
this._changeCallbacks.forEach((cb) => { this._changeCallbacks.forEach((cb) => {
cb(this._consent); 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. * 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. * @param callback The function to run when consent is granted.
*/ */
runWithConsent(callback: () => void) { runWithConsent(callback: () => void) {
if (this._consent) { if (this._consent) callback();
callback(); else this._consentQueue.push(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(() => { onDestroy(() => {
this._consentQueue = this._consentQueue.filter((cb) => cb !== callback); 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. * 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. * @returns The TrackingManager instance.
*/ */
export const getTrackingManager = (): TrackingManager => { export const getTrackingManager = (): TrackingManager => {
const saved = getContext<TrackingManager>(trackingManagerKey); try {
if (saved) return saved; 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; return manager;
}; };

94
src/lib/types/conversion.d.ts vendored Normal file
View File

@@ -0,0 +1,94 @@
import type { StandardEventName } from '../types/fbq.js';
/**
* Supported user data fields for conversion events.
*/
export type ConversionUserData = {
email?: string;
phone?: string;
firstName?: string;
lastName?: string;
ip?: string;
fbp?: string;
fbc?: string;
/** user agent */
ua?: string;
externalId?: string;
};
/**
* Supported custom data fields for conversion events.
*/
export type ConversionEventParams = {
value?: number;
net_revenue?: number;
currency?: string;
content_name?: string;
content_category?: string;
content_ids?: string[];
contents?: {
id?: string;
quantity?: number;
item_price?: number;
title?: string;
description?: string;
category?: string;
brand?: string;
delivery_category?: string;
}[];
content_type?: string;
order_id?: string;
predicted_ltv?: number;
num_items?: number;
search_string?: string;
status?: string;
item_number?: string;
delivery_category?: string;
custom_properties?: Record<string, unknown>;
};
/**
* Parameters for sending a conversion event to Meta Pixel.
*/
export type ConversionEventDetails = {
eventID?: string;
actionSource: 'website' | 'app' | 'offline' | 'other';
userData: ConversionUserData;
eventSourceURL?: string;
};
/**
* Request body for conversion event tracking.
*/
export type ConversionRequestBody = {
consent: boolean;
eventName: StandardEventName;
eventID: string;
eventSourceURL?: string;
utms?: Partial<
Record<'utm_source' | 'utm_medium' | 'utm_campaign' | 'utm_content' | 'utm_term', string>
>;
user?: Omit<ConversionUserData, 'ip'>;
customData?: ConversionEventParams;
};
/**
* Response body for conversion event tracking.
*/
export type ConversionResponseBody = {
/** 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[];
};
/** Error response body for conversion event tracking. */
export type ConversionErrorResponseBody = {
error: string;
};

View File

@@ -39,7 +39,7 @@ export type CommonParams = Partial<{
/** /**
* Product IDs associated with the event, such as SKUs (e.g. ["ABC123", "XYZ456"]). * Product IDs associated with the event, such as SKUs (e.g. ["ABC123", "XYZ456"]).
*/ */
content_ids: (string | number)[]; content_ids: string[];
/** /**
* Name of the page/product. * Name of the page/product.
@@ -340,6 +340,11 @@ export interface FBQ {
// consent and LDU // consent and LDU
(cmd: 'consent', state: 'grant' | 'revoke'): void; (cmd: 'consent', state: 'grant' | 'revoke'): void;
(cmd: 'dataProcessingOptions', options: string[], countryCode?: number, stateCode?: number): 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 { declare global {

View File

@@ -1,4 +1,5 @@
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import log from 'loglevel';
const SCRIPT_SRC = 'https://connect.facebook.net/en_US/fbevents.js'; const SCRIPT_SRC = 'https://connect.facebook.net/en_US/fbevents.js';
@@ -8,22 +9,60 @@ type QueuedFBQ = ((...args: unknown[]) => void) & {
loaded?: boolean; loaded?: boolean;
version?: string; version?: string;
push?: unknown; push?: unknown;
disablePushState?: boolean;
allowDuplicatePageViews?: boolean;
}; };
/** /**
* Loads the Meta Pixel script and configures the `fbq` function to queue * Loads the Meta Pixel script and configures the `fbq` function to queue
* commands until the script is fully loaded. You may optionally await the * commands until the script is fully loaded. You may optionally await the
* returned Promise to ensure the script has loaded before proceeding. * 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 // Make sure we're using the browser
if (!browser || !window) { 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 // If fbq is already defined, resolve immediately
const existing = window.fbq as QueuedFBQ | undefined; 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(); return Promise.resolve();
} }
@@ -40,18 +79,18 @@ export const loadMetaPixel = (): Promise<void> => {
q.push = q; q.push = q;
q.loaded = true; q.loaded = true;
q.version = '2.0'; 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;
window._fbq = q;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Avoid adding the same script twice // Avoid adding the same script twice
const existingScript = document.querySelector( const existingScript = getExistingScript();
`script[src="${SCRIPT_SRC}"]`
) as HTMLScriptElement | null;
if (existingScript) { if (existingScript) {
existingScript.addEventListener('load', () => resolve()); attachToScript(existingScript, resolve, reject);
existingScript.addEventListener('error', () => log.debug('Meta Pixel script already present, waiting for load');
reject(new Error('Failed to load Meta Pixel script'))
);
return; return;
} }
@@ -59,8 +98,23 @@ export const loadMetaPixel = (): Promise<void> => {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = SCRIPT_SRC; script.src = SCRIPT_SRC;
script.async = true; script.async = true;
script.addEventListener('load', () => resolve()); attachToScript(script, resolve, reject);
script.addEventListener('error', () => reject(new Error('Failed to load Meta Pixel script')));
document.head.appendChild(script); 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')));
};

View File

@@ -1,18 +1,10 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://svelte.dev/docs/kit/integrations // Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), 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()
}
}; };
export default config; export default config;