Compare commits
17 Commits
v0.0.2
...
0cd3f10da6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cd3f10da6 | ||
|
|
f2d389ee64 | ||
|
|
f5ec7b3812 | ||
|
|
fc07bb057c | ||
|
|
9400e81aaa | ||
|
|
824fd262ed | ||
|
|
82cce84a4e | ||
|
|
2e12d281ef | ||
|
|
99c1f003c6 | ||
|
|
97497db8e4 | ||
|
|
674663b027 | ||
|
|
e7b12f50b1 | ||
|
|
927b02d30e | ||
|
|
45a6bda53e | ||
|
|
9a72280737 | ||
|
|
095462c80d | ||
|
|
bb92e25485 |
@@ -4,7 +4,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.auvem.com/svelte-toolkit/spectator.git"
|
"url": "https://gitea.auvem.com/svelte-toolkit/spectator.git"
|
||||||
},
|
},
|
||||||
"version": "0.0.2",
|
"version": "0.0.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"svelte": "^5.0.0"
|
"svelte": "^5.40.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.4.0",
|
"@eslint/compat": "^1.4.0",
|
||||||
@@ -62,7 +62,10 @@
|
|||||||
"svelte"
|
"svelte"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/facebook-nodejs-business-sdk": "^23.0.0",
|
||||||
"@types/umami": "^2.10.1",
|
"@types/umami": "^2.10.1",
|
||||||
|
"facebook-nodejs-business-sdk": "^24.0.1",
|
||||||
|
"http-status-codes": "^2.3.0",
|
||||||
"loglevel": "^1.9.2"
|
"loglevel": "^1.9.2"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
259
pnpm-lock.yaml
generated
259
pnpm-lock.yaml
generated
@@ -8,9 +8,18 @@ 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:
|
loglevel:
|
||||||
specifier: ^1.9.2
|
specifier: ^1.9.2
|
||||||
version: 1.9.2
|
version: 1.9.2
|
||||||
@@ -473,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==}
|
||||||
|
|
||||||
@@ -565,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'}
|
||||||
@@ -578,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'}
|
||||||
@@ -605,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==}
|
||||||
|
|
||||||
@@ -621,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'}
|
||||||
@@ -640,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'}
|
||||||
@@ -716,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==}
|
||||||
|
|
||||||
@@ -742,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'}
|
||||||
@@ -749,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'}
|
||||||
@@ -766,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'}
|
||||||
@@ -800,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
|
||||||
@@ -848,6 +960,18 @@ packages:
|
|||||||
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==}
|
||||||
|
|
||||||
@@ -855,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'}
|
||||||
@@ -874,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'}
|
||||||
@@ -955,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'}
|
||||||
@@ -1447,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':
|
||||||
@@ -1567,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: {}
|
||||||
@@ -1580,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:
|
||||||
@@ -1603,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: {}
|
||||||
@@ -1615,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
|
||||||
@@ -1625,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
|
||||||
@@ -1752,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: {}
|
||||||
@@ -1771,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
|
||||||
@@ -1778,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
|
||||||
@@ -1789,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: {}
|
||||||
@@ -1814,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
|
||||||
@@ -1853,6 +2098,14 @@ snapshots:
|
|||||||
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
|
||||||
@@ -1861,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: {}
|
||||||
@@ -1871,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
|
||||||
@@ -1937,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
|
||||||
|
|||||||
@@ -1,221 +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;
|
|
||||||
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
|
|
||||||
* the base Meta Pixel script has not been loaded yet, it will be
|
|
||||||
* loaded automatically. Optionally sets a test event code for the Pixel.
|
|
||||||
* Does NOT initialize the Pixel; call `fireInit()` on the returned instance
|
|
||||||
* before tracking events.
|
|
||||||
* @param trackingManager Tracking manager to handle user consent for tracking
|
|
||||||
* @param pixelID Meta Pixel ID
|
|
||||||
* @param options Optional settings
|
|
||||||
* @returns PixelControl instance
|
|
||||||
*/
|
|
||||||
static for(
|
|
||||||
trackingManager: MaybeGetter<TrackingManager | undefined>,
|
|
||||||
pixelID: string,
|
|
||||||
options?: {
|
|
||||||
/**
|
|
||||||
* if provided, events fired will always have this code attached
|
|
||||||
* to prevent them from polluting real analytics data.
|
|
||||||
*/
|
|
||||||
testEventCode?: string;
|
|
||||||
}
|
|
||||||
): PixelControl {
|
|
||||||
PixelControl.load();
|
|
||||||
return new PixelControl(trackingManager, pixelID, options?.testEventCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes this pixel with the Meta Pixel API including any advanced
|
|
||||||
* matching data and options.
|
|
||||||
* @param advancedMatching Advanced matching data
|
|
||||||
* @param initOptions Initialization options
|
|
||||||
* @returns this PixelControl instance
|
|
||||||
*/
|
|
||||||
fireInit(advancedMatching?: AdvancedMatching, initOptions?: InitOptions): PixelControl {
|
|
||||||
PixelControl.loadGuard();
|
|
||||||
window.fbq('init', this._pixelID, advancedMatching, initOptions);
|
|
||||||
log.debug(`Meta Pixel [${this._pixelID}] initialized.`);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 });
|
|
||||||
log.debug(
|
|
||||||
`Meta Pixel [${this._pixelID}] PageView event sent (test 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
|
|
||||||
});
|
|
||||||
log.debug(
|
|
||||||
`Meta Pixel [${this._pixelID}] ${event} event sent (test 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
|
|
||||||
});
|
|
||||||
log.debug(
|
|
||||||
`Meta Pixel [${this._pixelID}] ${event} custom event sent (test 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';
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }).fireInit();
|
|
||||||
|
|
||||||
trackingManager.runWithConsent(() => {
|
|
||||||
if (autoPageView && pixel) {
|
|
||||||
pixel.pageView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onNavigate(() => {
|
|
||||||
trackingManager?.runWithConsent(() => {
|
|
||||||
if (autoPageView && pixel) {
|
|
||||||
pixel.pageView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -25,7 +25,9 @@
|
|||||||
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 => {
|
||||||
log.debug(`${devConsoleTag}: Track called with:`, ...args);
|
log.debug(`${devConsoleTag}: Track called with:`, ...args);
|
||||||
@@ -62,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dev) log.debug('[dev]: Umami tracking disabled');
|
if (dev) log.info('[Umami] [dev]: tracking disabled');
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (dev) {
|
if (dev) {
|
||||||
|
|||||||
92
src/lib/conversion/client.ts
Normal file
92
src/lib/conversion/client.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
src/lib/conversion/control.ts
Normal file
157
src/lib/conversion/control.ts
Normal 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))
|
||||||
|
.setEventId(details.eventID)
|
||||||
|
.setUserData(buildUserData(details.userData))
|
||||||
|
.setActionSource(details.actionSource);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/lib/conversion/index.ts
Normal file
3
src/lib/conversion/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './client.ts';
|
||||||
|
export * from './server.ts';
|
||||||
|
export * from './control.ts';
|
||||||
74
src/lib/conversion/server.ts
Normal file
74
src/lib/conversion/server.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const getEventIP = (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 }) => {
|
||||||
|
try {
|
||||||
|
const body = (await request.json()) as ConversionRequestBody;
|
||||||
|
|
||||||
|
// Build user data with IP and user agent
|
||||||
|
const ip = getEventIP(request, getClientAddress);
|
||||||
|
const ua = request.headers.get('user-agent');
|
||||||
|
|
||||||
|
const userData: ConversionUserData = {
|
||||||
|
...body.user,
|
||||||
|
ip,
|
||||||
|
ua: ua ?? body.user?.ua ?? undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
@@ -3,10 +3,12 @@
|
|||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
|
||||||
|
export type * from './types/conversion.d.ts';
|
||||||
export type * as fbq from './types/fbq.d.ts';
|
export type * as fbq from './types/fbq.d.ts';
|
||||||
export { default as MetaPixel, PixelControl } from './MetaPixel.svelte';
|
export * from './metapixel/index.ts';
|
||||||
export * from './tracking.svelte.ts';
|
export * from './tracking.svelte.ts';
|
||||||
export { default as Umami } 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
|
// set log level to debug if we're in dev mode
|
||||||
if (dev) {
|
if (dev) {
|
||||||
|
|||||||
76
src/lib/metapixel/MetaPixel.svelte
Normal file
76
src/lib/metapixel/MetaPixel.svelte
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<!-- @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 ensureFbcOptions: EnsureFbcOptions = $derived({
|
||||||
|
sameSite: 'Lax',
|
||||||
|
pixelLoaded: PixelControl.baseLoaded && !PixelControl.baseFailed
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!trackingManager) {
|
||||||
|
throw new Error('MetaPixel component requires a TrackingManager to manage consent.');
|
||||||
|
}
|
||||||
|
PixelControl.load();
|
||||||
|
pixel = PixelControl.initialize(trackingManager, pixelID, pixelOptions);
|
||||||
|
|
||||||
|
trackingManager.runWithConsent(() => {
|
||||||
|
if (autoPageView && pixel) {
|
||||||
|
pixel.pageView();
|
||||||
|
ensureFbc(ensureFbcOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onNavigate(() => {
|
||||||
|
trackingManager?.runWithConsent(() => {
|
||||||
|
if (autoPageView && pixel) {
|
||||||
|
pixel.pageView();
|
||||||
|
ensureFbc(ensureFbcOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getPixelControl = (): PixelControl => {
|
||||||
|
if (!pixel) {
|
||||||
|
throw new Error('MetaPixel component has not been initialized yet, wait for onMount.');
|
||||||
|
}
|
||||||
|
return pixel;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
88
src/lib/metapixel/fbc.ts
Normal file
88
src/lib/metapixel/fbc.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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 it’s 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.
|
||||||
|
*/
|
||||||
|
export function getFbpFbc(): { fbp?: string; fbc?: string } {
|
||||||
|
const fbp = getCookie('_fbp');
|
||||||
|
const fbc = getCookie('_fbc');
|
||||||
|
return { fbp, fbc };
|
||||||
|
}
|
||||||
4
src/lib/metapixel/index.ts
Normal file
4
src/lib/metapixel/index.ts
Normal 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';
|
||||||
320
src/lib/metapixel/pixel-control.ts
Normal file
320
src/lib/metapixel/pixel-control.ts
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
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 = $state(false);
|
||||||
|
private static _baseFailed: boolean = $state(false);
|
||||||
|
private static _registeredPixels: Record<string, PixelControl> = {};
|
||||||
|
|
||||||
|
/** Indicates whether the Meta Pixel base script has been loaded. */
|
||||||
|
static get baseLoaded(): boolean {
|
||||||
|
return this._baseLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Indicates whether the Meta Pixel base script has failed to load. */
|
||||||
|
static get baseFailed(): boolean {
|
||||||
|
return this._baseFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 async load() {
|
||||||
|
if (this._baseLoaded && !this.baseFailed && !!window.fbq) return;
|
||||||
|
if (!window.fbq) {
|
||||||
|
try {
|
||||||
|
await loadMetaPixel(); // Load the Meta Pixel script
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('[PixelControl] Failed to load Meta Pixel script, all events will be queued.', e);
|
||||||
|
this._baseFailed = true;
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
this._baseLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug('[PixelControl] 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('[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
|
||||||
|
* @throws Error if the Meta Pixel is not initialized.
|
||||||
|
*/
|
||||||
|
pageView() {
|
||||||
|
if (!this.consentGuard()) return;
|
||||||
|
|
||||||
|
let eventID: string | undefined = undefined;
|
||||||
|
// Optionally, send to conversion API endpoint if configured
|
||||||
|
if (this._conversionClient) {
|
||||||
|
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)
|
||||||
|
* @throws Error if the Meta Pixel is not initialized.
|
||||||
|
*/
|
||||||
|
track<K extends StandardEventName>(event: K, params?: EventParamsByName[K], eventID?: string) {
|
||||||
|
if (!this.consentGuard()) return;
|
||||||
|
|
||||||
|
// Optionally, send to conversion API endpoint if configured
|
||||||
|
if (this._conversionClient) {
|
||||||
|
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.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -78,7 +132,7 @@ export class TrackingManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether tracking is consented. 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.
|
||||||
*/
|
*/
|
||||||
setConsent(value: boolean) {
|
setConsent(value: boolean) {
|
||||||
if (this._consent === value) return;
|
if (this._consent === value) return;
|
||||||
@@ -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
94
src/lib/types/conversion.d.ts
vendored
Normal 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;
|
||||||
|
};
|
||||||
7
src/lib/types/fbq.d.ts
vendored
7
src/lib/types/fbq.d.ts
vendored
@@ -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 {
|
||||||
|
|||||||
@@ -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')));
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user