@omega.js/manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/LICENSE +98 -0
- package/README.md +165 -0
- package/bin/omega +2 -0
- package/bin/omg +2 -0
- package/claude-plugin/.claude-plugin/plugin.json +11 -0
- package/claude-plugin/.mcp.json +9 -0
- package/claude-plugin/README.md +139 -0
- package/claude-plugin/hooks/gate/mark.sh +58 -0
- package/claude-plugin/hooks/gate/run.sh +97 -0
- package/claude-plugin/hooks/guard/run.sh +249 -0
- package/claude-plugin/hooks/hooks.json +63 -0
- package/claude-plugin/hooks/inject/run.sh +127 -0
- package/claude-plugin/hooks/lib/omega-gate.sh +20 -0
- package/claude-plugin/hooks/lib/omega-scope.sh +41 -0
- package/claude-plugin/hooks/lib/omega-skills.sh +209 -0
- package/claude-plugin/hooks/quality/run.sh +132 -0
- package/claude-plugin/hooks/shape/run.sh +45 -0
- package/claude-plugin/mcp-router-launch.js +29 -0
- package/claude-plugin/skills/README.md +48 -0
- package/claude-plugin/skills/accessibility/SKILL.md +31 -0
- package/claude-plugin/skills/analytics/SKILL.md +30 -0
- package/claude-plugin/skills/backend/SKILL.md +25 -0
- package/claude-plugin/skills/brandcheck/SKILL.md +29 -0
- package/claude-plugin/skills/browser/SKILL.md +64 -0
- package/claude-plugin/skills/client/SKILL.md +24 -0
- package/claude-plugin/skills/desktop/SKILL.md +25 -0
- package/claude-plugin/skills/extension/SKILL.md +25 -0
- package/claude-plugin/skills/main/SKILL.md +54 -0
- package/claude-plugin/skills/manager/SKILL.md +25 -0
- package/claude-plugin/skills/seo/SKILL.md +36 -0
- package/claude-plugin/skills/theme/SKILL.md +33 -0
- package/claude-plugin/skills/web/SKILL.md +29 -0
- package/dist/cli-run.js +34 -0
- package/dist/cli.js +29 -0
- package/dist/commands/build.js +17 -0
- package/dist/commands/clean.js +16 -0
- package/dist/commands/company.js +46 -0
- package/dist/commands/deploy.js +143 -0
- package/dist/commands/dev.js +437 -0
- package/dist/commands/devlog.js +25 -0
- package/dist/commands/help.js +48 -0
- package/dist/commands/manage.js +53 -0
- package/dist/commands/migrate.js +76 -0
- package/dist/commands/onboard.js +31 -0
- package/dist/commands/pipeline.js +319 -0
- package/dist/commands/test.js +380 -0
- package/dist/commands/update.js +128 -0
- package/dist/commands/version.js +8 -0
- package/dist/company-init.js +167 -0
- package/dist/company.js +283 -0
- package/dist/config.js +1359 -0
- package/dist/devlog/index.js +226 -0
- package/dist/devlog/lib/collect-commits.js +136 -0
- package/dist/devlog/lib/generate-post.js +201 -0
- package/dist/devlog/lib/ghostii.js +122 -0
- package/dist/devlog/lib/project-map.js +98 -0
- package/dist/devlog/lib/publish-website.js +88 -0
- package/dist/index.js +45 -0
- package/dist/lib/agents-md.js +201 -0
- package/dist/lib/analytics-secret.js +22 -0
- package/dist/lib/argv.js +21 -0
- package/dist/lib/auth-admin.js +120 -0
- package/dist/lib/automation-client.js +210 -0
- package/dist/lib/backend-marketing.js +49 -0
- package/dist/lib/brand.js +240 -0
- package/dist/lib/bundle-id.js +53 -0
- package/dist/lib/claude-settings.js +125 -0
- package/dist/lib/company-scaffold.js +213 -0
- package/dist/lib/company.js +227 -0
- package/dist/lib/config-flow.js +350 -0
- package/dist/lib/config-write.js +41 -0
- package/dist/lib/custom-target.js +75 -0
- package/dist/lib/domain-utils.js +19 -0
- package/dist/lib/duration.js +39 -0
- package/dist/lib/env-order.js +236 -0
- package/dist/lib/env-secret.js +78 -0
- package/dist/lib/firestore-rest.js +326 -0
- package/dist/lib/framework-bin.js +156 -0
- package/dist/lib/gitignore.js +63 -0
- package/dist/lib/google-auth.js +495 -0
- package/dist/lib/google-token.js +66 -0
- package/dist/lib/jwt.js +31 -0
- package/dist/lib/legacy-oauth.js +59 -0
- package/dist/lib/node-version.js +127 -0
- package/dist/lib/owner-plan.js +78 -0
- package/dist/lib/package-scripts.js +152 -0
- package/dist/lib/preflight.js +470 -0
- package/dist/lib/product-create.js +144 -0
- package/dist/lib/run-command.js +74 -0
- package/dist/lib/run-gates.js +70 -0
- package/dist/lib/run-output.js +42 -0
- package/dist/lib/run-summary.js +384 -0
- package/dist/lib/scaffold.js +427 -0
- package/dist/lib/service-input.js +207 -0
- package/dist/lib/service-runner.js +466 -0
- package/dist/lib/stale.js +24 -0
- package/dist/lib/target-selection.js +155 -0
- package/dist/lib/verb-fanout.js +128 -0
- package/dist/lib/verify-live.js +188 -0
- package/dist/manage.js +311 -0
- package/dist/omega-bin.js +7 -0
- package/dist/onboard.js +532 -0
- package/dist/services/account/ensure/users.js +259 -0
- package/dist/services/account/index.js +97 -0
- package/dist/services/account/lib/backend-client.js +107 -0
- package/dist/services/account/lib/password.js +36 -0
- package/dist/services/account/lib/resolve-password.js +100 -0
- package/dist/services/advertising/ensure/sites.js +101 -0
- package/dist/services/advertising/index.js +101 -0
- package/dist/services/advertising/lib/adsense-api.js +41 -0
- package/dist/services/ai/ensure/keys.js +21 -0
- package/dist/services/ai/index.js +36 -0
- package/dist/services/analytics/ensure/google-firebase-link.js +230 -0
- package/dist/services/analytics/ensure/google-streams.js +256 -0
- package/dist/services/analytics/ensure/meta-pixel.js +22 -0
- package/dist/services/analytics/ensure/tiktok-pixel.js +22 -0
- package/dist/services/analytics/index.js +134 -0
- package/dist/services/analytics/lib/analytics-api.js +156 -0
- package/dist/services/analytics/lib/meta-api.js +83 -0
- package/dist/services/analytics/lib/pixel-account.js +78 -0
- package/dist/services/analytics/lib/pixel-provision.js +152 -0
- package/dist/services/analytics/lib/pixel-specs.js +57 -0
- package/dist/services/analytics/lib/pixel-token.js +88 -0
- package/dist/services/analytics/lib/property-flow.js +77 -0
- package/dist/services/analytics/lib/tiktok-api.js +157 -0
- package/dist/services/analytics/lib/tiktok-auth.js +153 -0
- package/dist/services/assets/ensure/logo-gen.js +68 -0
- package/dist/services/assets/index.js +109 -0
- package/dist/services/assets/lib/assets-config.js +168 -0
- package/dist/services/assets/lib/brandmark-api.js +160 -0
- package/dist/services/assets/lib/derived.js +113 -0
- package/dist/services/assets/lib/font-loader.js +67 -0
- package/dist/services/assets/lib/reconcile.js +122 -0
- package/dist/services/assets/lib/reset.js +118 -0
- package/dist/services/assets/lib/svg-logo-generator.js +154 -0
- package/dist/services/assets/lib/svg-to-black.js +32 -0
- package/dist/services/assets/write/favicons.js +82 -0
- package/dist/services/assets/write/icons.js +64 -0
- package/dist/services/assets/write/process.js +96 -0
- package/dist/services/assets/write/reconcile.js +30 -0
- package/dist/services/assets/write/social-icons.js +108 -0
- package/dist/services/assets/write/templates.js +345 -0
- package/dist/services/bookmark/ensure/sync.js +255 -0
- package/dist/services/bookmark/index.js +17 -0
- package/dist/services/campaigns/ensure/contact-person.js +40 -0
- package/dist/services/campaigns/ensure/custom-fields.js +84 -0
- package/dist/services/campaigns/ensure/domain-auth.js +92 -0
- package/dist/services/campaigns/ensure/event-webhook.js +89 -0
- package/dist/services/campaigns/ensure/link-branding.js +187 -0
- package/dist/services/campaigns/ensure/list.js +50 -0
- package/dist/services/campaigns/ensure/segments.js +109 -0
- package/dist/services/campaigns/ensure/sender-identity.js +153 -0
- package/dist/services/campaigns/ensure/unsubscribe-groups.js +116 -0
- package/dist/services/campaigns/index.js +79 -0
- package/dist/services/campaigns/lib/dns-sync.js +81 -0
- package/dist/services/campaigns/lib/segment-query.js +138 -0
- package/dist/services/campaigns/lib/sendgrid-api.js +287 -0
- package/dist/services/captcha/ensure/site-key.js +83 -0
- package/dist/services/captcha/index.js +78 -0
- package/dist/services/captcha/lib/console-url.js +45 -0
- package/dist/services/captcha/lib/recaptcha-api.js +36 -0
- package/dist/services/certificates/ensure/api-key.js +22 -0
- package/dist/services/certificates/ensure/bundle-ids.js +115 -0
- package/dist/services/certificates/ensure/certificates.js +187 -0
- package/dist/services/certificates/ensure/profiles.js +143 -0
- package/dist/services/certificates/index.js +225 -0
- package/dist/services/certificates/lib/apple-api.js +183 -0
- package/dist/services/certificates/lib/certificate-manager.js +252 -0
- package/dist/services/certificates/lib/identifier-manager.js +107 -0
- package/dist/services/certificates/lib/keychain.js +135 -0
- package/dist/services/certificates/lib/manual-walkthrough.js +165 -0
- package/dist/services/certificates/lib/profile-manager.js +115 -0
- package/dist/services/chat/data/baseline-knowledge.md +96 -0
- package/dist/services/chat/ensure/chat.js +106 -0
- package/dist/services/chat/ensure/user.js +45 -0
- package/dist/services/chat/index.js +139 -0
- package/dist/services/chat/lib/baseline-knowledge.js +137 -0
- package/dist/services/cloud/ensure/authentication.js +267 -0
- package/dist/services/cloud/ensure/billing.js +120 -0
- package/dist/services/cloud/ensure/cloud-messaging.js +105 -0
- package/dist/services/cloud/ensure/database.js +60 -0
- package/dist/services/cloud/ensure/firestore.js +79 -0
- package/dist/services/cloud/ensure/functions.js +49 -0
- package/dist/services/cloud/ensure/hosting.js +398 -0
- package/dist/services/cloud/ensure/oauth-consent.js +248 -0
- package/dist/services/cloud/ensure/project-settings.js +72 -0
- package/dist/services/cloud/ensure/sdk-config.js +95 -0
- package/dist/services/cloud/ensure/service-account.js +152 -0
- package/dist/services/cloud/ensure/services.js +103 -0
- package/dist/services/cloud/ensure/storage.js +47 -0
- package/dist/services/cloud/index.js +130 -0
- package/dist/services/cloud/lib/access-heal.js +130 -0
- package/dist/services/cloud/lib/firebase-api.js +662 -0
- package/dist/services/cloud/lib/project-flow.js +102 -0
- package/dist/services/directory/ensure/entry.js +48 -0
- package/dist/services/directory/index.js +64 -0
- package/dist/services/directory/lib/blocks.js +104 -0
- package/dist/services/disperse/index.js +45 -0
- package/dist/services/disperse/write/certs.js +117 -0
- package/dist/services/domain/ensure/nameservers.js +179 -0
- package/dist/services/domain/index.js +69 -0
- package/dist/services/domain/lib/namecheap-api.js +167 -0
- package/dist/services/domain/lib/registrars.js +46 -0
- package/dist/services/domain/lib/whitelist-walkthrough.js +76 -0
- package/dist/services/edge/ensure/cache-rules.js +105 -0
- package/dist/services/edge/ensure/dns-records.js +304 -0
- package/dist/services/edge/ensure/email-routing.js +292 -0
- package/dist/services/edge/ensure/rules-configuration.js +98 -0
- package/dist/services/edge/ensure/rules-managed-transforms.js +101 -0
- package/dist/services/edge/ensure/rules-redirect.js +107 -0
- package/dist/services/edge/ensure/rules-response-headers.js +90 -0
- package/dist/services/edge/ensure/rules-security.js +142 -0
- package/dist/services/edge/ensure/speed-scheduled-tests.js +98 -0
- package/dist/services/edge/ensure/workers.js +219 -0
- package/dist/services/edge/ensure/zone-settings.js +108 -0
- package/dist/services/edge/ensure/zone.js +183 -0
- package/dist/services/edge/index.js +73 -0
- package/dist/services/edge/lib/cloudflare-api.js +84 -0
- package/dist/services/edge/lib/dns-records-helpers.js +518 -0
- package/dist/services/edge/lib/read-cache.js +16 -0
- package/dist/services/edge/lib/ruleset-helper.js +108 -0
- package/dist/services/edge/workers/omega-api-proxy.js +43 -0
- package/dist/services/email/data/baseline-filter.md +4 -0
- package/dist/services/email/data/baseline-knowledge.md +86 -0
- package/dist/services/email/ensure/agent.js +107 -0
- package/dist/services/email/ensure/user.js +45 -0
- package/dist/services/email/index.js +138 -0
- package/dist/services/email/lib/baseline.js +82 -0
- package/dist/services/email/lib/knowledge-file.js +53 -0
- package/dist/services/forms/ensure/form.js +46 -0
- package/dist/services/forms/ensure/user.js +45 -0
- package/dist/services/forms/index.js +138 -0
- package/dist/services/migrations/ensure/notifications.js +189 -0
- package/dist/services/migrations/ensure/orders.js +34 -0
- package/dist/services/migrations/ensure/payment-provider.js +132 -0
- package/dist/services/migrations/ensure/payments-intents.js +34 -0
- package/dist/services/migrations/ensure/state-retirement.js +266 -0
- package/dist/services/migrations/ensure/targets-rename.js +124 -0
- package/dist/services/migrations/ensure/users.js +1176 -0
- package/dist/services/migrations/index.js +101 -0
- package/dist/services/migrations/lib/attribution-touch.js +64 -0
- package/dist/services/migrations/lib/ensure-metadata.js +144 -0
- package/dist/services/migrations/lib/migration-runner.js +473 -0
- package/dist/services/migrations/lib/sanitize-strings.js +112 -0
- package/dist/services/migrations/lib/schema-validator.js +154 -0
- package/dist/services/monitoring/ensure/dsn.js +54 -0
- package/dist/services/monitoring/ensure/projects.js +135 -0
- package/dist/services/monitoring/index.js +54 -0
- package/dist/services/monitoring/lib/sentry-api.js +94 -0
- package/dist/services/newsletter/ensure/custom-fields.js +90 -0
- package/dist/services/newsletter/ensure/publication.js +94 -0
- package/dist/services/newsletter/ensure/segments.js +140 -0
- package/dist/services/newsletter/ensure/webhook.js +108 -0
- package/dist/services/newsletter/index.js +64 -0
- package/dist/services/newsletter/lib/beehiiv-api.js +124 -0
- package/dist/services/newsletter/lib/segment-automation.js +577 -0
- package/dist/services/payment/ensure/chargebee-account.js +30 -0
- package/dist/services/payment/ensure/chargebee-products.js +359 -0
- package/dist/services/payment/ensure/chargebee-webhook.js +113 -0
- package/dist/services/payment/ensure/paypal-account.js +36 -0
- package/dist/services/payment/ensure/paypal-products.js +374 -0
- package/dist/services/payment/ensure/paypal-webhook.js +128 -0
- package/dist/services/payment/ensure/stripe-account.js +93 -0
- package/dist/services/payment/ensure/stripe-disputes.js +56 -0
- package/dist/services/payment/ensure/stripe-products.js +245 -0
- package/dist/services/payment/ensure/stripe-radar.js +74 -0
- package/dist/services/payment/ensure/stripe-webhook.js +130 -0
- package/dist/services/payment/index.js +163 -0
- package/dist/services/payment/lib/chargebee-api.js +338 -0
- package/dist/services/payment/lib/payment-utils.js +116 -0
- package/dist/services/payment/lib/paypal-api.js +354 -0
- package/dist/services/payment/lib/provider-setup.js +134 -0
- package/dist/services/payment/lib/stripe-api.js +152 -0
- package/dist/services/repo/ensure/org.js +70 -0
- package/dist/services/repo/ensure/pages.js +72 -0
- package/dist/services/repo/ensure/repo.js +90 -0
- package/dist/services/repo/index.js +52 -0
- package/dist/services/repo/lib/github-api.js +196 -0
- package/dist/services/search/ensure/ga-link.js +57 -0
- package/dist/services/search/ensure/property.js +137 -0
- package/dist/services/search/ensure/sitemaps.js +70 -0
- package/dist/services/search/index.js +61 -0
- package/dist/services/search/lib/search-console-api.js +92 -0
- package/dist/services/seo/ensure/github-repos.js +271 -0
- package/dist/services/seo/index.js +55 -0
- package/dist/services/seo/lib/gh-api.js +161 -0
- package/dist/services/seo/templates/developer-tool/.github/workflows/maintenance.yml +25 -0
- package/dist/services/seo/templates/developer-tool/.nvmrc +1 -0
- package/dist/services/seo/templates/developer-tool/_README.md.js +121 -0
- package/dist/services/seo/templates/developer-tool/_package.json.js +33 -0
- package/dist/services/seo/templates/developer-tool/src/index.js +120 -0
- package/dist/services/seo/templates/index.js +57 -0
- package/dist/services/server/ensure/brands.js +63 -0
- package/dist/services/server/index.js +42 -0
- package/dist/services/testing/ensure/target-checks.js +97 -0
- package/dist/services/testing/index.js +11 -0
- package/dist/services/testing/lib/checks.js +467 -0
- package/dist/services/update/index.js +20 -0
- package/dist/services/update/lib/cache.js +59 -0
- package/dist/services/update/lib/fingerprint.js +157 -0
- package/dist/services/update/write/targets.js +206 -0
- package/dist/services/workspace/ensure/agents.js +42 -0
- package/dist/services/workspace/ensure/claude-settings.js +34 -0
- package/dist/services/workspace/ensure/config.js +54 -0
- package/dist/services/workspace/ensure/defaults.js +60 -0
- package/dist/services/workspace/ensure/env-keys.js +55 -0
- package/dist/services/workspace/ensure/env-order.js +66 -0
- package/dist/services/workspace/ensure/env-rules.js +97 -0
- package/dist/services/workspace/ensure/gitignore.js +18 -0
- package/dist/services/workspace/ensure/scripts.js +126 -0
- package/dist/services/workspace/ensure/structure.js +97 -0
- package/dist/services/workspace/ensure/translation-sdk.js +148 -0
- package/dist/services/workspace/ensure/workflows.js +54 -0
- package/dist/services/workspace/index.js +8 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/certs.js +199 -0
- package/dist/vendor/devkit/ci-workflows.js +520 -0
- package/dist/vendor/devkit/cli-router.js +156 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/flows.js +317 -0
- package/dist/vendor/devkit/local-https.js +360 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/prompt.js +187 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/translate/cache.js +84 -0
- package/dist/vendor/devkit/translate/engine.js +243 -0
- package/dist/vendor/devkit/translate/index.js +49 -0
- package/dist/vendor/devkit/translate/languages.js +134 -0
- package/dist/vendor/devkit/translate/providers.js +188 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/docs/AGENTS.md +200 -0
- package/docs/account.md +45 -0
- package/docs/advertising.md +46 -0
- package/docs/ai.md +34 -0
- package/docs/analytics.md +69 -0
- package/docs/assets.md +57 -0
- package/docs/bookmark.md +26 -0
- package/docs/brand.md +151 -0
- package/docs/campaigns.md +96 -0
- package/docs/captcha.md +46 -0
- package/docs/certificates.md +68 -0
- package/docs/chat.md +47 -0
- package/docs/cloud.md +110 -0
- package/docs/company.md +64 -0
- package/docs/directory.md +140 -0
- package/docs/disperse.md +46 -0
- package/docs/domain.md +56 -0
- package/docs/edge.md +234 -0
- package/docs/email.md +42 -0
- package/docs/forms.md +48 -0
- package/docs/index.md +109 -0
- package/docs/migration.md +203 -0
- package/docs/migrations.md +56 -0
- package/docs/monitoring.md +41 -0
- package/docs/newsletter.md +48 -0
- package/docs/payment.md +83 -0
- package/docs/repo.md +48 -0
- package/docs/search.md +44 -0
- package/docs/seo.md +34 -0
- package/docs/server.md +37 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +34 -0
- package/docs/update.md +36 -0
- package/docs/workspace.md +64 -0
- package/package.json +88 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
# Analytics — the catalog, the one call, and where an event fires
|
|
2
|
+
|
|
3
|
+
> Every event any OMEGA surface counts — a web page, the desktop main process, the
|
|
4
|
+
> extension, a Cloud Function — is declared ONCE in a catalog and fired through ONE
|
|
5
|
+
> call, and each provider hears it in its own dialect
|
|
6
|
+
> ([#328](https://github.com/Omega-JS-Stack/omega/issues/328)).
|
|
7
|
+
|
|
8
|
+
This file is the CONTRACT, not a migration report: the package and its catalog are built
|
|
9
|
+
([#382](https://github.com/Omega-JS-Stack/omega/issues/382)), the web consent gate is built
|
|
10
|
+
([#383](https://github.com/Omega-JS-Stack/omega/issues/383)), the server-side delivery is
|
|
11
|
+
built ([#385](https://github.com/Omega-JS-Stack/omega/issues/385)), and every front-end call
|
|
12
|
+
site now fires through the facade ([#386](https://github.com/Omega-JS-Stack/omega/issues/386)).
|
|
13
|
+
New code writes to what is below.
|
|
14
|
+
|
|
15
|
+
## The shape
|
|
16
|
+
|
|
17
|
+
`@omega.js/analytics` is a private workspace package — it never publishes, and prepare
|
|
18
|
+
vendors it into the published frameworks (the `@omega.js/account` pattern, HARD RULE 3 in
|
|
19
|
+
the repo map; [publishing.md](publishing.md)). It is CJS, because a Cloud Function and the
|
|
20
|
+
Electron main process `require()` it while the browser bundles import it with standard
|
|
21
|
+
interop.
|
|
22
|
+
|
|
23
|
+
| File (`packages/analytics/src/`) | What it owns |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `index.js` | The facade — `event()`, `configure()`, and the fire walk |
|
|
26
|
+
| `catalog.js` | The SSOT: one entry per canonical event, with every provider mapping |
|
|
27
|
+
| `adapters/resolve.js` | The shared adapter mechanism (catalog lookup → mapping → payload) |
|
|
28
|
+
| `adapters/ga4.js` · `meta.js` · `tiktok.js` | One file per provider: its consent category and where attribution attaches |
|
|
29
|
+
| `transports/browser.js` | Executes a descriptor against the page globals, guarded ([#306](https://github.com/Omega-JS-Stack/omega/issues/306)) |
|
|
30
|
+
| `consent.js` | The consent seam — `createConsentGate(providerFn)`, categories, `GRANT_ALL` |
|
|
31
|
+
| `core.js` | GA4 Measurement Protocol semantics (device_id/client_id/user_id derivation, payload shape) |
|
|
32
|
+
| `logger.js` | This package's `[@omega.js/analytics:<module>]` tag ([logging.md](logging.md)) |
|
|
33
|
+
|
|
34
|
+
**The one call**, from client code and backend code alike:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
analytics.event('purchase', { transaction_id: id, value: 19, currency: 'USD', items: [...] });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
One fire walks three steps per provider, in this order:
|
|
41
|
+
|
|
42
|
+
1. **consent** — a blocked category's providers never even resolve
|
|
43
|
+
2. **adapter** — this provider's catalog mapping, or `null` when it has none
|
|
44
|
+
3. **transport** — the host's seam; a missing or blocked page global is a silent no-op
|
|
45
|
+
|
|
46
|
+
Nothing in that walk throws at a visitor. A transport that could not deliver returns
|
|
47
|
+
`false`, and the dev log is the only trace. That guard is the WHOLE contract for an
|
|
48
|
+
unconfigured or blocked provider ([#606](https://github.com/Omega-JS-Stack/omega/issues/606)):
|
|
49
|
+
no surface defines a placeholder `gtag`/`fbq`/`ttq`, because a stub only makes a pixel
|
|
50
|
+
that never loaded read as present. @omega.js/web's page chrome carries none, and
|
|
51
|
+
`test/analytics-blocked.test.js` fails any template or module that names one.
|
|
52
|
+
|
|
53
|
+
**The host injects its seams** — nothing is sniffed:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
analytics.configure({ transport, consent, context, environment });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`transport` is `{ send(descriptor) => boolean }`; `consent` is a gate from
|
|
60
|
+
`createConsentGate`; `context` is `{ attribution, consent, runtime }`; `environment` mirrors
|
|
61
|
+
the client's `config.environment` seam and defaults to `'production'`. An unconfigured
|
|
62
|
+
runtime is INERT — it resolves and logs, and delivers nothing — never a guess at page
|
|
63
|
+
globals.
|
|
64
|
+
|
|
65
|
+
**Who injects what**, per runtime:
|
|
66
|
+
|
|
67
|
+
| Host | transport | consent | context | environment |
|
|
68
|
+
|---|---|---|---|---|
|
|
69
|
+
| web page (`@omega.js/web` `core/js/libs/analytics.js`) | the guarded browser transport | the banner's record, read live | attribution + `runtime: 'web'` | — |
|
|
70
|
+
| `@omega.js/client` | the browser transport on web; the Measurement Protocol in the extension; NONE in a desktop renderer, which forwards over the IPC bridge instead ([#411](https://github.com/Omega-JS-Stack/omega/issues/411)) | — | `runtime` in the extension | the brand's `config.environment` |
|
|
71
|
+
| `@omega.js/desktop` main process | its own Measurement Protocol fetch, and the ONE sender for the whole install — its own windows' events included | — | `runtime: 'electron'` | the app's dev flag |
|
|
72
|
+
| `@omega.js/backend` | the three HTTP APIs (`libraries/analytics/conversions.js`) | the order's/user's snapshot | the order's attribution | — |
|
|
73
|
+
|
|
74
|
+
Web core reaches the package THROUGH `@omega.js/client` (`@omega.js/client/modules/analytics.js`),
|
|
75
|
+
never as a bare `@omega.js/analytics`: the package is private, so in a consumer install it
|
|
76
|
+
exists only as the copy vendored into the client's dist — and the client is a real runtime
|
|
77
|
+
dependency of every framework.
|
|
78
|
+
|
|
79
|
+
**A fire may also carry options**: `event(name, params, { eventId, providers })`. `eventId`
|
|
80
|
+
is the platform dedupe key both halves of a two-sided event name (Meta's fourth pixel
|
|
81
|
+
argument `eventID`, TikTok's `event_id`). GA4 has no event-id slot — it deduplicates
|
|
82
|
+
ecommerce on the `transaction_id` PARAM instead
|
|
83
|
+
([Google](https://support.google.com/analytics/answer/12313109),
|
|
84
|
+
[#656](https://github.com/Omega-JS-Stack/omega/issues/656)), which is why both halves of a
|
|
85
|
+
`purchase` send the same one. `providers` restricts a fire to the providers THIS side owns —
|
|
86
|
+
which is how a `both` event splits without double-counting.
|
|
87
|
+
|
|
88
|
+
**An unknown event name is a programmer error.** It throws in development (a typo must not
|
|
89
|
+
silently cost a conversion) and is logged-and-skipped in production, where the throw would
|
|
90
|
+
take the customer's action with it.
|
|
91
|
+
|
|
92
|
+
## The catalog contract
|
|
93
|
+
|
|
94
|
+
One entry per canonical event; callers only ever speak canonical names and canonical
|
|
95
|
+
params. There is no unified cross-provider signature — each adapter speaks its provider's
|
|
96
|
+
dialect (Ian's constraint). The canonical vocabulary is GA4-flavoured (flat params plus an
|
|
97
|
+
`items` array), so GA4 is pass-through and the `map()`s live where the ad platforms
|
|
98
|
+
genuinely disagree.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
add_to_cart: {
|
|
102
|
+
params: ['currency', 'value', 'items'],
|
|
103
|
+
placement: 'client',
|
|
104
|
+
providers: {
|
|
105
|
+
ga4: { name: 'add_to_cart', kind: 'standard' },
|
|
106
|
+
meta: { name: 'AddToCart', kind: 'standard', map: metaCommerce },
|
|
107
|
+
tiktok: { name: 'AddToCart', kind: 'standard', map: tiktokCommerce },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- **`params`** — the canonical param contract; documentation for callers.
|
|
113
|
+
- **`placement`** — `'client' | 'server' | 'both'` (the rule below).
|
|
114
|
+
- **`providers`** — the per-provider mapping. Each carries `name` (the provider's NATIVE
|
|
115
|
+
event name), `kind`, and an optional `map(params, context) => payload` (default:
|
|
116
|
+
pass-through).
|
|
117
|
+
- **`method`** — the rare fourth key, browser only: the provider's own PIXEL METHOD to call
|
|
118
|
+
instead of its tracked-event command, for a signal the platform manages itself rather than
|
|
119
|
+
exposing as a trackable name. Exactly one mapping names one today, TikTok's `page_view` →
|
|
120
|
+
`ttq.page()` ([#409](https://github.com/Omega-JS-Stack/omega/issues/409)); it takes no name
|
|
121
|
+
and no payload, the pixel reads the page, and `name`/`kind` stay declared for the catalog
|
|
122
|
+
and the fire log.
|
|
123
|
+
- **`actionSource`** — the fifth key, Meta only and SERVER only: the `action_source` this
|
|
124
|
+
event's conversion carries instead of the default `website`. Meta requires the field and
|
|
125
|
+
asks that it be accurate, and its enum names the case itself, `system_generated`, "for
|
|
126
|
+
example, a subscription renewal that's set to auto-pay each month". So
|
|
127
|
+
`subscription_renew`, `payment_recovered` and `trial_convert` declare it: a card was billed
|
|
128
|
+
with nobody on the site ([#498](https://github.com/Omega-JS-Stack/omega/issues/498)). A
|
|
129
|
+
cancellation the subscriber clicked is a website conversion like any other and declares
|
|
130
|
+
nothing.
|
|
131
|
+
|
|
132
|
+
**The two commerce dialects**, as the 2026-09-02 standards review against the live references
|
|
133
|
+
left them ([#498](https://github.com/Omega-JS-Stack/omega/issues/498)). The browser and the
|
|
134
|
+
server halves send the SAME payload for one event, because that is what they deduplicate on:
|
|
135
|
+
|
|
136
|
+
- **Meta** `custom_data` carries `content_ids` AND `contents` (`{ id, quantity, item_price }`,
|
|
137
|
+
Meta's own field names; its pixel reference asks for `contents` or `content_ids` on a catalog campaign),
|
|
138
|
+
`content_type: 'product'`, `currency`, `value`, `num_items` (the UNITS bought, summed over
|
|
139
|
+
the items, not the number of lines), `order_id` from the canonical `transaction_id`, and
|
|
140
|
+
`content_name` only on an event with exactly ONE product, since Meta documents it as the
|
|
141
|
+
singular name of the page/product.
|
|
142
|
+
- **TikTok** `properties` carries `content_ids` AND `contents`
|
|
143
|
+
(`{ content_id, content_name, price, quantity }`), `content_type: 'product'` at the
|
|
144
|
+
PROPERTIES level where Events API 2.0 documents it (its `contents` object has no per-item
|
|
145
|
+
`content_type`; the older Pixel SDK model's does), `currency`, `value` (the order total,
|
|
146
|
+
where a `price` is one item's) and `order_id`.
|
|
147
|
+
- A churn moment's exclusion signal keeps neither platform's priced fields: Meta's `contents`
|
|
148
|
+
comes off whole (its `item_price` IS the amount, and the object requires `id` + `quantity`),
|
|
149
|
+
TikTok's keeps the array minus `price`/`quantity`, and `content_ids` names the plan on both.
|
|
150
|
+
|
|
151
|
+
**The three mapping kinds** — and the third one is an absence:
|
|
152
|
+
|
|
153
|
+
| Kind | Meaning | What the transport does |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `standard` | The platform defines this event | Meta `fbq('track', …)`, the platform's own event |
|
|
156
|
+
| `custom` | A deliberate custom event (often preserving today's data continuity, e.g. Meta `ExitPopupShow`) | Meta `fbq('trackCustom', …)` |
|
|
157
|
+
| *absent* | This provider has no mapping for this event | The adapter returns `null`; the facade dev-logs `skipped (no mapping)` |
|
|
158
|
+
|
|
159
|
+
**Unmapped stays unmapped.** An absent mapping is a decision, not a gap to fill: inventing a
|
|
160
|
+
custom event so a provider "has coverage" ships junk into an ad account nobody will ever
|
|
161
|
+
read. Equally, a mapping must be HONEST — several pre-OMEGA call sites claimed standard
|
|
162
|
+
names their platform does not define, and those are `custom` here.
|
|
163
|
+
|
|
164
|
+
**Every name, kind and param is checked LIVE against the platform's own spec before it is
|
|
165
|
+
written or changed** (Ian 2026-08-27,
|
|
166
|
+
[#652](https://github.com/Omega-JS-Stack/omega/issues/652)) — never from memory, because a
|
|
167
|
+
platform renames and retires events (TikTok retired `CompletePayment` for `Purchase`, and
|
|
168
|
+
`ClickButton` was never on its standard list at all). The pages sit in the catalog
|
|
169
|
+
header so no edit can miss them:
|
|
170
|
+
|
|
171
|
+
| Spec | What it settles |
|
|
172
|
+
|---|---|
|
|
173
|
+
| [GA4 recommended events](https://support.google.com/analytics/answer/9267735) | Which canonical names are GA4 standards |
|
|
174
|
+
| [Meta standard events](https://www.facebook.com/business/help/402791146561655) | The exact wire name, and what each event MEANS (Purchase requires `value` + `currency`) |
|
|
175
|
+
| [Meta Pixel reference](https://developers.facebook.com/docs/meta-pixel/reference) | Which params each standard event takes, and which a catalog campaign needs (`contents` or `content_ids`) |
|
|
176
|
+
| [TikTok standard events](https://ads.tiktok.com/help/article/standard-events-parameters) | The current standard roster and its recommended params |
|
|
177
|
+
| [TikTok Events API 2.0](https://business-api.tiktok.com/portal/docs?id=1771100865818625) | The server half's envelope, `properties` and `contents` fields (the 2.0 shape, which is not the Pixel SDK's) |
|
|
178
|
+
| [GA4 transaction_id dedupe](https://support.google.com/analytics/answer/12313109) | The one PARAM that binds: one id per charge |
|
|
179
|
+
|
|
180
|
+
A `kind: 'standard'` may only ever claim a name the platform lists; anything else is
|
|
181
|
+
`custom`, under the canonical event's own PascalCase name. `packages/analytics/test/catalog.test.js`
|
|
182
|
+
pins both rosters, so a mapping that drifts off them fails.
|
|
183
|
+
|
|
184
|
+
**Adding an event is one catalog entry plus a test.** The entry lands in `catalog.js` under
|
|
185
|
+
its section, and `packages/analytics/test/catalog.test.js` gets the resolve case that proves
|
|
186
|
+
the mapping (name, kind, and the payload each provider receives). The shape guards —
|
|
187
|
+
placement valid, every mapping carrying a name and a known kind — already run over the whole
|
|
188
|
+
catalog, so a malformed entry fails without new test code.
|
|
189
|
+
|
|
190
|
+
**Naming.** Canonical names are snake_case and follow GA4's standard names where one exists
|
|
191
|
+
(`file_download`, not `download`). A custom name is `{category}_{action}` with the action in
|
|
192
|
+
PRESENT/imperative form — `trial_start`, `subscription_cancel`, `user_delete`, never the past
|
|
193
|
+
tense of the fact ([#416](https://github.com/Omega-JS-Stack/omega/issues/416)) — because that
|
|
194
|
+
is how GA4, Meta and TikTok all name a conversion; a platform's own standard WIRE name stays
|
|
195
|
+
verbatim wherever a mapping uses one (Meta and TikTok both spell it `StartTrial`), but a PascalCase
|
|
196
|
+
wire WE coin for a `custom` mapping obeys the same tense rule as the canonical it carries
|
|
197
|
+
(`SubscriptionCancel`, `Refund`, never the past-tense form). Action buckets
|
|
198
|
+
stay buckets — `user_billing_action`, `user_refund_request`, `user_section_view` carry an
|
|
199
|
+
`action`/`section` param rather than sprawling into an event per button.
|
|
200
|
+
Two events that look similar are only merged when they
|
|
201
|
+
answer the same question: `marketing_newsletter_subscribe` and `status_subscribe` are deliberately
|
|
202
|
+
distinct. A family's names move TOGETHER — the marketing opt-ins and their opt-outs all carry
|
|
203
|
+
the `marketing_` prefix (`marketing_email_subscribe` / `marketing_email_unsubscribe`), while
|
|
204
|
+
`status_subscribe` keeps none because status-page updates are operational, not marketing.
|
|
205
|
+
|
|
206
|
+
## The placement rule
|
|
207
|
+
|
|
208
|
+
**Money and account truth fires SERVER-side. UI actions fire CLIENT-side. `sign_up` fires
|
|
209
|
+
BOTH.**
|
|
210
|
+
|
|
211
|
+
| Placement | Why | Examples |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| `server` | The browser cannot be trusted for revenue, and the outcome is only known where it happened | `refund`, `trial_convert`, `trial_lapse`, `subscription_cancel`, `subscription_uncancel`, `subscription_plan_change`, `subscription_renew`, `payment_recovered`, `user_delete` |
|
|
214
|
+
| `client` | The event IS the interaction — nothing server-side ever sees it | `view_item`, `add_to_cart`, `begin_checkout`, `add_payment_info`, `page_view`, `file_download`, the exit-popup, consent, notification-permission and account-navigation events |
|
|
215
|
+
| `both` | Ad platforms need the browser signal for retargeting AND the server signal for truth | `sign_up`, `purchase`, `trial_start` |
|
|
216
|
+
|
|
217
|
+
When both halves fire, both must carry the SAME event id so the platform collapses them into
|
|
218
|
+
one conversion (Meta's deduplication, TikTok's `event_id`). Sending both without the id is
|
|
219
|
+
double-counting; sending only the server half loses the retargeting signal — which is exactly
|
|
220
|
+
the hole the commented-out purchase pixel left
|
|
221
|
+
([#302](https://github.com/Omega-JS-Stack/omega/issues/302)).
|
|
222
|
+
|
|
223
|
+
**GA4 deduplicates `purchase` on `transaction_id`** — web streams only, and never on an
|
|
224
|
+
empty string ([Google](https://support.google.com/analytics/answer/12313109)). It has no
|
|
225
|
+
event-id slot, so which providers each half owns is decided per event, and the three `both`
|
|
226
|
+
events split it three different ways:
|
|
227
|
+
|
|
228
|
+
| Event | The dedupe id | Browser half | Server half |
|
|
229
|
+
|---|---|---|---|
|
|
230
|
+
| `sign_up` | `sign_up.<uid>` — the uid is the only thing both sides hold before either fires | GA4 + Meta + TikTok (`libs/auth/tracking.js`) | Meta + TikTok (`routes/user/signup` → `libraries/analytics/signup.js`, the post-auth request — never the auth trigger, which has no request behind it, [#577](https://github.com/Omega-JS-Stack/omega/issues/577)) |
|
|
231
|
+
| `purchase` | `purchase.<order id>` for the ad platforms, and the same `<order id>` as `transaction_id` for GA4 | GA4 + Meta + TikTok (`pages/payment/confirmation/modules/tracking.js`) | GA4 + Meta + TikTok (`events/firestore/payments-webhooks/analytics.js`) |
|
|
232
|
+
| `trial_start` | `trial_start.<order id>` | Meta + TikTok (`pages/payment/confirmation/modules/tracking.js`) — GA4's `trial_start` is a CUSTOM event with no dedupe, so the server keeps it | GA4 + Meta + TikTok (`events/firestore/payments-webhooks/analytics.js`) |
|
|
233
|
+
|
|
234
|
+
GA4 rides the browser purchase because the id makes it safe, and the browser is where the
|
|
235
|
+
session, the campaign and the `client_id` are — a webhook has none of them
|
|
236
|
+
([#656](https://github.com/Omega-JS-Stack/omega/issues/656)). And the browser half fires
|
|
237
|
+
`trial_start`, not `purchase`, on a trial checkout: `?track=true` rides EVERY checkout, and
|
|
238
|
+
two different event NAMES never deduplicate, so the old browser Purchase collapsed into
|
|
239
|
+
nothing and each platform counted it as a second conversion beside the server's trial. The
|
|
240
|
+
value was never the defect — the intent route already sends `amount=0` on a trial's
|
|
241
|
+
confirmation URL ([#654](https://github.com/Omega-JS-Stack/omega/issues/654)).
|
|
242
|
+
|
|
243
|
+
**`transaction_id` is ONE CHARGE's id, never the subscription's** (Ian 2026-08-27):
|
|
244
|
+
|
|
245
|
+
| The charge | The id | Why |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| First purchase — a paid checkout, a one-time buy, or a trial start | the ORDER id | The only id the confirmation page holds, so both halves match |
|
|
248
|
+
| Renewal, recovery, trial conversion | the provider's own charge id (Stripe/Chargebee invoice, PayPal sale), carried on the webhook event as `chargeId` | A subscription bills against one order forever; the subscription id collapsed every renewal after the first into a GA4 duplicate |
|
|
249
|
+
| Refund | the id of the charge it reverses — the provider's `chargeId` on a subscription, the ORDER id on a one-time buy (its single charge, and no provider names one on the refund) | So GA4 nets the two |
|
|
250
|
+
| Everything else (cancel, uncancel, plan change, lapse) | the subscription id | A GA4 CUSTOM event with no dedupe, and the subscription really is its subject |
|
|
251
|
+
| THE EXCEPTION — the trial-lapse sweep's own `trial_convert` (`events/cron/daily/trial-lapse-sweep.js`) | the subscription id | The provider never announced the charge (PayPal fires no trial-end event at all), so this path has no invoice to name. `trial.outcome` is stamped once, which keeps it to one fire |
|
|
252
|
+
|
|
253
|
+
Revenue is the server's, because a browser cannot be trusted with it; a registration's GA4
|
|
254
|
+
count is the browser's, because that is where the funnel it belongs to lives.
|
|
255
|
+
|
|
256
|
+
An entry's `placement` is where the rule is written down per event, so a call site in the
|
|
257
|
+
wrong tier is a reviewable finding, not a judgment call.
|
|
258
|
+
|
|
259
|
+
## Consent
|
|
260
|
+
|
|
261
|
+
Nothing counts before the visitor's answer is known. Two tiers, chosen by the browser's own
|
|
262
|
+
timezone — no network call, nothing to consent to before consent exists
|
|
263
|
+
(`packages/web/core/js/libs/consent-region.js`):
|
|
264
|
+
|
|
265
|
+
- **Opt-in regions — a STRICT ROSTER**: `Europe/*` (the EEA, the UK, and Turkey, which rides
|
|
266
|
+
the prefix), the four Atlantic zones the EEA reaches outside it (Reykjavik, Canary, Madeira,
|
|
267
|
+
Azores), Brazil's sixteen zones (LGPD), China (PIPL: Asia/Shanghai, Asia/Urumqi) and South
|
|
268
|
+
Korea (PIPA: Asia/Seoul). No provider script loads until Accept, and the banner is the GATE
|
|
269
|
+
that asks. Google Consent Mode's `consent default` is queued into `dataLayer` before gtag.js
|
|
270
|
+
can load (denied), and every change pushes a `consent update`, so the tag itself honors the
|
|
271
|
+
flags on top of us not loading it. A timezone we cannot place reads as opt-in — the only safe
|
|
272
|
+
direction.
|
|
273
|
+
- **The join rule**: a country joins only when its law genuinely requires opt-in consent for
|
|
274
|
+
tracking cookies and that is verified; when in doubt it stays out
|
|
275
|
+
([#423](https://github.com/Omega-JS-Stack/omega/issues/423)). Quebec's Law 25 qualifies but
|
|
276
|
+
cannot be expressed — America/Montreal aliases to America/Toronto — so Canada stays out.
|
|
277
|
+
- **Everywhere else**: auto opt-in, scripts load immediately, and a first visit sees **no
|
|
278
|
+
banner at all** ([#391](https://github.com/Omega-JS-Stack/omega/issues/391)) — only the
|
|
279
|
+
small Cookies Settings tab, which reopens the full panel for anyone who wants to turn things
|
|
280
|
+
off. There is nothing to gate and nothing worth interrupting, so `cookie_banner_show` fires
|
|
281
|
+
only where the banner actually shows.
|
|
282
|
+
|
|
283
|
+
The record is `trackingConsent` in the client storage blob
|
|
284
|
+
(`packages/web/core/js/libs/tracking-consent.js`): `{ analytics, marketing, region,
|
|
285
|
+
timestamp, version }`. Two categories, because that is what a visitor can meaningfully
|
|
286
|
+
answer — `analytics` (GA4) and `marketing` (Meta, TikTok); "necessary" is not a category
|
|
287
|
+
because nothing about it is optional. Bumping `TRACKING_CONSENT_VERSION` re-prompts everyone,
|
|
288
|
+
which is what changing what a category COVERS requires.
|
|
289
|
+
|
|
290
|
+
- The banner is `packages/web/core/js/core/consent.js`: a cookie-iconed message, one big
|
|
291
|
+
Accept, and a small Customize opening the panel — a dense legal intro, a row of pill
|
|
292
|
+
switches (Necessary locked on, then Analytics and Marketing), then **Accept all / Accept
|
|
293
|
+
none** bottom-right. Every switch APPLIES AND SAVES the moment it is flipped and the panel
|
|
294
|
+
stays open, so there is no Save to forget to press; either button flips every switch to
|
|
295
|
+
match, saves, and collapses to the tab. Refusal is one click at the same size as the grant
|
|
296
|
+
(the EU equal-ease rule). The loader that actually gates the scripts is
|
|
297
|
+
`packages/web/core/js/core/analytics-loader.js`, and it runs FIRST in `main.js` — nothing
|
|
298
|
+
may count an event ahead of that decision. That loader counts NOTHING of its own: the Meta
|
|
299
|
+
and TikTok page views it used to fire raw at pixel init now go through the facade like every
|
|
300
|
+
other event ([#409](https://github.com/Omega-JS-Stack/omega/issues/409)), restricted to the
|
|
301
|
+
pixel that just installed, since GA4 counts its own page view off the `config` command.
|
|
302
|
+
- **A save that grants nothing IS the denial** — Accept none and an all-off panel are the
|
|
303
|
+
same answer and fire the same `cookie_consent_deny`. That fire is made AFTER the record is
|
|
304
|
+
written, so the gate the visitor just closed is the one it is asked about: a denial is
|
|
305
|
+
honored, not counted.
|
|
306
|
+
- Granting a category injects its loader immediately, no reload. REVOKING cannot unload a
|
|
307
|
+
running script, so it takes effect on the next page load while Consent Mode updates now.
|
|
308
|
+
- In the package, each adapter declares its `CONSENT_CATEGORY` and the facade asks the gate
|
|
309
|
+
once per provider per event. The gate reads its provider function LIVE, so a visitor who
|
|
310
|
+
accepts mid-session is counted from that moment with nothing re-configuring. The default
|
|
311
|
+
when no host injects a gate is `GRANT_ALL` — desktop, extension and backend have no banner
|
|
312
|
+
to gate on.
|
|
313
|
+
- The same consent state gates the server-side match-data enrichment
|
|
314
|
+
([#302](https://github.com/Omega-JS-Stack/omega/issues/302)): an opted-out user sends
|
|
315
|
+
nothing, from either side.
|
|
316
|
+
- **Absence is not denial.** Server-side, a null or absent `trackingConsent` snapshot
|
|
317
|
+
GRANTS both categories — only an explicit `false` blocks. Legacy orders predating the
|
|
318
|
+
consent system and the raw-API recovery lane (which mints orders with no client anywhere
|
|
319
|
+
near them) carry no snapshot at all, and refusing to report their revenue would be a
|
|
320
|
+
silent accounting hole rather than a privacy win.
|
|
321
|
+
|
|
322
|
+
**The `consent` name is taken.** `consent` in storage and in the signup payload is the
|
|
323
|
+
signup form's LEGAL record (`{ legal, marketing }`, captured by
|
|
324
|
+
`packages/web/core/js/libs/auth/forms.js` and interpreted by the backend signup route).
|
|
325
|
+
Tracking consent is `trackingConsent` — key, exports, and payload field — everywhere. Sharing
|
|
326
|
+
one key would read a tracking answer as a revoked terms agreement.
|
|
327
|
+
|
|
328
|
+
## Attribution
|
|
329
|
+
|
|
330
|
+
Attribution is captured once and CARRIED; no event assembles its own.
|
|
331
|
+
|
|
332
|
+
- **The shape**: `attribution: { first, last, affiliate }` in client storage — first-touch
|
|
333
|
+
written on the first visit ever seen and never overwritten, last-touch replaced only by a
|
|
334
|
+
TAGGED visit, both timestamped (any lookback window is computed at read time).
|
|
335
|
+
- **Where it attaches**: the user doc at signup, `payments-intents` at creation, and
|
|
336
|
+
`payments-orders` via the intent fold. The tracking-consent snapshot rides the same
|
|
337
|
+
payloads under `trackingConsent`.
|
|
338
|
+
- **The reserved slot**: every event carries attribution through the facade's `context`, not
|
|
339
|
+
through its params — `configure({ context })` → `resolve(name, params, context)` → each
|
|
340
|
+
adapter attaches it the way ITS provider wants: GA4 takes campaign fields as flat event
|
|
341
|
+
params, while Meta (`fbc`, `fbp`) and TikTok (`ttclid`, `ttp`) take theirs in the
|
|
342
|
+
descriptor's `userData` match block, never in the event's custom data. The touch's
|
|
343
|
+
`url`/`referrer` ride the same slot server-side ([#497](https://github.com/Omega-JS-Stack/omega/issues/497)):
|
|
344
|
+
Meta sends `event_source_url`, TikTok a `page` object, GA4 nothing — omitted entirely
|
|
345
|
+
when the touch carried no url. A call site never hand-attaches attribution.
|
|
346
|
+
|
|
347
|
+
Capture and storage are [#384](https://github.com/Omega-JS-Stack/omega/issues/384); the
|
|
348
|
+
server-side delivery with full match data (hashed email/phone, IP, user agent, click ids) is
|
|
349
|
+
[#385](https://github.com/Omega-JS-Stack/omega/issues/385);
|
|
350
|
+
[#302](https://github.com/Omega-JS-Stack/omega/issues/302) owns the deep spec for both.
|
|
351
|
+
|
|
352
|
+
## Running paid ads
|
|
353
|
+
|
|
354
|
+
Two systems count a paid campaign, they disagree by design, and each is right about a
|
|
355
|
+
different question. This is the reading guide, plus the one thing every campaign has to do
|
|
356
|
+
for either of them to work.
|
|
357
|
+
|
|
358
|
+
**Every paid ad link carries utm tags.** The system can only credit what the LINK declares:
|
|
359
|
+
the landing capture reads the utm tags off the URL, stores them as that visit's touch, and
|
|
360
|
+
carries the touch onto the user doc, the payment intent and the order. An untagged ad click
|
|
361
|
+
is an organic visit forever, in both ledgers below. So the destination URL of every ad, on
|
|
362
|
+
every platform, is tagged:
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
https://brand.test/pricing?utm_source=meta&utm_medium=cpc&utm_campaign=launch-2026&utm_content=hero-video&utm_term=project-management
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
| Tag | What it names |
|
|
369
|
+
|---|---|
|
|
370
|
+
| `utm_source` | The platform the click came from (`meta`, `tiktok`, `google`, `newsletter`) |
|
|
371
|
+
| `utm_medium` | How it was paid for (`cpc`, `paid_social`, `email`) |
|
|
372
|
+
| `utm_campaign` | The campaign, spelled the SAME way it is spelled in the ads manager |
|
|
373
|
+
| `utm_content` | The creative, so two ads in one campaign stay tellable apart |
|
|
374
|
+
| `utm_term` | The keyword or audience (optional: search and interest targeting) |
|
|
375
|
+
|
|
376
|
+
The platform's own click id (`fbclid`, `ttclid`, `gclid`) rides along on its own and is what
|
|
377
|
+
the server's match data links a conversion to. The utm tags are how a HUMAN reads the result.
|
|
378
|
+
|
|
379
|
+
**The two ledgers.** Both are honest; they measure different windows with different rules.
|
|
380
|
+
|
|
381
|
+
| Ledger | What it counts | Read it for |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| The ads manager (Meta Events Manager, TikTok Ads) | GROSS, short window: the conversions its own pixel and Conversions/Events API saw inside its attribution window, at the value that fired, credited its own way | Steering spend day to day, and feeding the optimizer the signal it bids on |
|
|
384
|
+
| GA4, `first user campaign` | NET, long run: everything a campaign's visitors ever did, renewals included and refunds netted out, credited to the campaign that FIRST brought them | Whether the campaign was worth running at all |
|
|
385
|
+
|
|
386
|
+
The ads manager runs ahead and reads high: it counts a conversion the moment its window says
|
|
387
|
+
so, it cannot subtract a refund, and each platform credits itself for clicks the other also
|
|
388
|
+
saw. GA4 runs behind and reads low: a renewal thirteen months out still lands on the campaign
|
|
389
|
+
that acquired the customer, and a refund nets against the purchase it reverses. Never subtract
|
|
390
|
+
one from the other and never average them. **The ads manager decides what to do today; GA4
|
|
391
|
+
decides whether to keep doing it.**
|
|
392
|
+
|
|
393
|
+
**Exclusion audiences are what a churn event is for.** Neither ad platform can subtract
|
|
394
|
+
revenue and neither optimizes against an event, but both can build an AUDIENCE from a custom
|
|
395
|
+
one ([#415](https://github.com/Omega-JS-Stack/omega/issues/415)). So the two churn moments
|
|
396
|
+
reach them as custom events worth zero: `subscription_cancel` as `SubscriptionCancel`,
|
|
397
|
+
`refund` as `Refund`, both `value: 0` because sending the real amount would ADD to the
|
|
398
|
+
return the ads manager reports. Build the platform's exclusion audience off those two, and
|
|
399
|
+
spend stops chasing people who already left. `trial_lapse` is deliberately NOT in that lane:
|
|
400
|
+
a lapsed trialist is a win-back audience worth RETARGETING, not somebody to hide ads from.
|
|
401
|
+
|
|
402
|
+
**The Measurement Protocol secret ships inside extension and desktop bundles, and that is an
|
|
403
|
+
accepted tradeoff** ([#413](https://github.com/Omega-JS-Stack/omega/issues/413), Ian's ruling
|
|
404
|
+
2026-08-20). Those runtimes have no page pixels, so they report through GA4's Measurement
|
|
405
|
+
Protocol, which requires an `api_secret` in the request: a distributed bundle therefore
|
|
406
|
+
carries one, and anyone who unpacks it can extract it. The exposure is bounded and known: an
|
|
407
|
+
MP secret is WRITE-ONLY, it reads nothing back, and the worst an abuser can do is post junk
|
|
408
|
+
events into the property. That is data POLLUTION, not data theft, and it is a documented GA4
|
|
409
|
+
limitation rather than a bug here. The reopen trigger is real abuse: if junk events or secret
|
|
410
|
+
misuse ever show up in a live property, reopen #413 and spec the backend proxy (every
|
|
411
|
+
extension event routed through a Cloud Function, which is the cost the ruling declined to pay
|
|
412
|
+
up front).
|
|
413
|
+
|
|
414
|
+
## Dev logging — one line per fire
|
|
415
|
+
|
|
416
|
+
In development the whole walk prints as ONE tagged line, the complete per-provider outcome
|
|
417
|
+
of that event:
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
[@omega.js/analytics:events] add_to_cart → ga4 sent, meta sent, tiktok skipped (consent: marketing)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
The outcome vocabulary is the walk itself: `sent`, `skipped (consent: <category>)`,
|
|
424
|
+
`skipped (no mapping)`, `skipped (no transport)`, `blocked (no global)` — which is how a
|
|
425
|
+
blocker looks, since a blocked global is a silent no-op by contract. Production prints
|
|
426
|
+
nothing per fire.
|
|
427
|
+
|
|
428
|
+
This one line is the whole dev trace. It replaced web core's `setupTrackingInterceptors()`
|
|
429
|
+
(`packages/web/core/js/libs/dev.js`), which retired with the call-site rewire
|
|
430
|
+
([#386](https://github.com/Omega-JS-Stack/omega/issues/386)): monkey-patching the globals
|
|
431
|
+
only ever saw the calls that survived the page's own guards, while the walk above reports the
|
|
432
|
+
skips too, and why.
|
|
433
|
+
|
|
434
|
+
## Only production reaches a platform
|
|
435
|
+
|
|
436
|
+
**A server conversion is delivered in PRODUCTION and nowhere else.** An emulator boot seeds
|
|
437
|
+
personas, every seeded account fired the server half of `sign_up`, and Meta delivered dozens
|
|
438
|
+
of fake registrations to the brand's live pixel — a dev run polluting the ad data the
|
|
439
|
+
optimizer bids on ([#464](https://github.com/Omega-JS-Stack/omega/issues/464)). So
|
|
440
|
+
`deliverConversion` carries the gate `@omega.js/monitoring` and the Measurement Protocol
|
|
441
|
+
helper already carry: any non-production environment (development OR testing, the intentional
|
|
442
|
+
`!isProduction()` check) delivers nothing.
|
|
443
|
+
|
|
444
|
+
The gate sits AFTER the catalog resolve, so the walk still reports what WOULD have fired and
|
|
445
|
+
with what — a blocked send is information, not silence. Each blocked provider says so on its
|
|
446
|
+
own line, and the fire's summary line carries the outcome word `blocked (dev)` plus the match
|
|
447
|
+
keys that fire would have carried (providers separated by ` | `, since each summary has commas
|
|
448
|
+
of its own):
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
[@omega.js/backend:omega_api] deliverConversion [meta]: sign_up blocked (dev) — nothing sent (event_id=sign_up.<uid>)
|
|
452
|
+
[@omega.js/backend:omega_api] deliverConversion: sign_up → ga4 skipped (not selected) | meta blocked (dev) sent em,fn,ln,external_id,client_ip_address,client_user_agent,fbp; empty ph,ct,st,zp,country,db,ge,fbc | tiktok blocked (dev) sent … (event_id=sign_up.<uid>)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
A real send is therefore proved the way **Verifying** says below — a live drive read in the
|
|
456
|
+
platform's own debugger — never by pointing a dev run at the live pixel.
|
|
457
|
+
|
|
458
|
+
## Identity is not an event
|
|
459
|
+
|
|
460
|
+
An identity has no catalog entry, because it is not something that happened — it is a SETTING
|
|
461
|
+
the events after it inherit. Each provider takes its own: GA4's `set` (`user_id`, user
|
|
462
|
+
properties), the Meta Pixel's advanced-matching `init`, TikTok's `identify`. So it lives
|
|
463
|
+
beside the transport rather than inside the catalog, in exactly one module per runtime:
|
|
464
|
+
|
|
465
|
+
- **web** — `identify(user)` / `reset()` in `core/js/libs/analytics.js`, called by
|
|
466
|
+
`core/js/core/auth.js` off the auth state, guarded per provider like every other page call.
|
|
467
|
+
It sets GA4's user PROPERTIES, the Meta `init` and the TikTok `identify`. `external_id` is
|
|
468
|
+
each platform's own key and rides in the shape that platform's spec asks for: RAW for Meta,
|
|
469
|
+
which only RECOMMENDS hashing and whose own Pixel example passes a bare id, and SHA-256 for
|
|
470
|
+
TikTok, whose Events API REQUIRES the digest
|
|
471
|
+
([#410](https://github.com/Omega-JS-Stack/omega/issues/410)). Every OTHER match
|
|
472
|
+
key is SHA-256 hashed before it reaches a pixel, normalized per that platform's own spec
|
|
473
|
+
(Meta hashes a phone as bare digits, TikTok as E.164), and the rules live in
|
|
474
|
+
`@omega.js/analytics/identity`. The server's match data agrees key for key: `match-data.js`
|
|
475
|
+
reads the SAME normalizers out of that module and carries both digests
|
|
476
|
+
(`metaPhoneHash` / `tiktokPhoneHash`, the browser's own key names, plus
|
|
477
|
+
`tiktokExternalIdHash`), so a pixel event and a server conversion for one person present
|
|
478
|
+
identical match keys
|
|
479
|
+
([#392](https://github.com/Omega-JS-Stack/omega/issues/392)).
|
|
480
|
+
- **every runtime** — `@omega.js/client`'s `setUserId` / `setUserProperties`, which SEND on
|
|
481
|
+
web through the page's gtag and ride the Measurement Protocol payload elsewhere. The one
|
|
482
|
+
carve-out is a bridged desktop renderer: main owns identity, so its renderer forwards
|
|
483
|
+
`setUserProperties` over the bridge and `setUserId` throws loudly
|
|
484
|
+
([#480](https://github.com/Omega-JS-Stack/omega/issues/480)). The
|
|
485
|
+
cross-surface value is `uuidv5(uid, namespace)`: the same human is the same `user_id` on a
|
|
486
|
+
page, in the desktop app, in the extension and from a Cloud Function.
|
|
487
|
+
|
|
488
|
+
**GA4's `user_id` has exactly ONE owner: the client.** Both modules run on a web page off the
|
|
489
|
+
same auth transition, so a second writer is simply the last writer — and web writing the raw
|
|
490
|
+
uid there would leave GA4 holding an id no other surface ever sends. `identify()` therefore
|
|
491
|
+
sets no `user_id`, and `reset()` clears none.
|
|
492
|
+
|
|
493
|
+
The client also fires `login` / `logout` off auth state — on every runtime EXCEPT web, where
|
|
494
|
+
the auth pages own them, because only the call site knows the method the visitor actually
|
|
495
|
+
used.
|
|
496
|
+
|
|
497
|
+
### The device id every `client_id` starts from
|
|
498
|
+
|
|
499
|
+
`client_id` is `uuidv5(deviceId, namespace)`, and the deviceId under it comes from ONE
|
|
500
|
+
derivation — `core.deriveDeviceId({ get, set, seed })` — with each target injecting its own
|
|
501
|
+
world, on the `createRequest(deps)` mold
|
|
502
|
+
([#396](https://github.com/Omega-JS-Stack/omega/issues/396)):
|
|
503
|
+
|
|
504
|
+
| Target | Persistence | Seed |
|
|
505
|
+
|---|---|---|
|
|
506
|
+
| web · extension (`@omega.js/client`) | `localStorage._omega_device_id` | none — a page can read nothing about the machine, so the generated UUID IS the id |
|
|
507
|
+
| desktop main (`lib/context.js`), for the whole install | electron-store `context.deviceId` | the first non-internal MAC, so a reinstalled app resolves the id it had before its storage was wiped |
|
|
508
|
+
|
|
509
|
+
A desktop RENDERER derives none and persists none ([#411](https://github.com/Omega-JS-Stack/omega/issues/411)):
|
|
510
|
+
its client is bridged, its events are delivered by main's sender, and main's id is the
|
|
511
|
+
install's identity. A renderer minting its own would be the identity fork the bridge exists to
|
|
512
|
+
prevent — one install counted as two GA clients.
|
|
513
|
+
|
|
514
|
+
The walk is **stored → seed → uuidv4**: a persisted id always wins (a desktop install survives
|
|
515
|
+
a NIC swap or a VPN), and the floor is the `uuid` package's `v4`, which is a real UUID in every
|
|
516
|
+
runtime — it uses the platform's `crypto.randomUUID` where that exists and `getRandomValues`
|
|
517
|
+
where it does not (an insecure origin), so no surface falls back to a random-looking string.
|
|
518
|
+
|
|
519
|
+
**One machine is NOT one GA client across surfaces**, and never was: each surface derives its
|
|
520
|
+
device id from its own storage, so the desktop app and the browser on one machine are two
|
|
521
|
+
`client_id`s. What unifies a human is `user_id`, and it rides ALONGSIDE the client id in every
|
|
522
|
+
payload rather than replacing it — providers need the stable client id for session stitching.
|
|
523
|
+
|
|
524
|
+
## Match quality — every parameter, and where its value comes from
|
|
525
|
+
|
|
526
|
+
**The plumbing exists for EVERY match parameter each platform accepts, even where a brand does
|
|
527
|
+
not yet hold the value** (Ian's ruling, 2026-08-24,
|
|
528
|
+
[#577](https://github.com/Omega-JS-Stack/omega/issues/577)). An empty value compacts away as
|
|
529
|
+
it always has; what is never acceptable is a parameter the platform reads and we never wired.
|
|
530
|
+
Meta scored the playground's StartTrial 6.2/10 and its CompleteRegistration around 4/10 while
|
|
531
|
+
every server event carried `em` + `external_id` alone — and the account doc already held the
|
|
532
|
+
name, birthday, gender, location and phone.
|
|
533
|
+
|
|
534
|
+
**One normalization table per provider, and no shared "close enough" normalizer.** The rules
|
|
535
|
+
genuinely differ key by key, and a value normalized by the wrong platform's rule is ACCEPTED
|
|
536
|
+
by the API and matched to nobody — the same silent nothing an unhashed value is. The server's
|
|
537
|
+
tables live in `packages/backend/src/manager/libraries/analytics/match-data.js`; the browser
|
|
538
|
+
half's shared rules (email, phone, external id) live in `@omega.js/analytics/identity` so both
|
|
539
|
+
halves of one person present identical keys.
|
|
540
|
+
|
|
541
|
+
**Meta** — Conversions API `user_data`
|
|
542
|
+
([customer-information parameters](https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters)).
|
|
543
|
+
Every key below is hashing-required except the last four.
|
|
544
|
+
|
|
545
|
+
| Key | Source | Normalization |
|
|
546
|
+
|---|---|---|
|
|
547
|
+
| `em` | `auth.email` (Auth's record on a signup) | trim, lowercase, SHA-256 |
|
|
548
|
+
| `ph` | `personal.telephone` `{ countryCode, national }` | digits only, country code included, SHA-256 |
|
|
549
|
+
| `fn` / `ln` | `personal.name.first` / `.last` | lowercase, no punctuation, SHA-256 |
|
|
550
|
+
| `ct` | `personal.location.city` | lowercase, no punctuation and NO SPACES (`newyork`), SHA-256 |
|
|
551
|
+
| `st` | `personal.location.region` | the 2-character ANSI code in lowercase (a US state NAME is looked up); other countries lowercase with no spaces, SHA-256 |
|
|
552
|
+
| `zp` | `personal.location.postalCode` — the account page's optional postal-code field ([#663](https://github.com/Omega-JS-Stack/omega/issues/663)); the bag key keeps Meta's own name | lowercase, no spaces or dashes, first five digits of a US zip, SHA-256 |
|
|
553
|
+
| `country` | `personal.location.country` | ISO 3166-1 alpha-2, lowercase, SHA-256 — a country NAME sends nothing |
|
|
554
|
+
| `db` | `personal.birthday` (the `$timestamp` pair) | `YYYYMMDD` in UTC, SHA-256 |
|
|
555
|
+
| `ge` | `personal.gender` | the lowercase initial, and Meta accepts `f`/`m` alone — anything else sends nothing |
|
|
556
|
+
| `external_id` | the uid | RAW (Meta only RECOMMENDS hashing, #410) |
|
|
557
|
+
| `client_ip_address` / `client_user_agent` | the request that created the checkout intent, or the post-auth signup request | never hashed |
|
|
558
|
+
| `fbc` / `fbp` | the browser's `_fbc`/`_fbp` cookies; `fbc` is CONSTRUCTED from a captured `fbclid` when the cookie never arrived | never hashed |
|
|
559
|
+
|
|
560
|
+
**TikTok** — Events API 2.0 `data[].user`
|
|
561
|
+
([event/track/](https://business-api.tiktok.com/portal/docs?id=1771101303285761)): `email`,
|
|
562
|
+
`phone`, `external_id` (all three SHA-256 REQUIRED), `ttclid`, `ttp`, `ip`, `user_agent`.
|
|
563
|
+
**There is no name or address parameter** — its user object documents none, so the backend
|
|
564
|
+
sends none. A `first_name` or `zip_code` key there is read by nobody.
|
|
565
|
+
|
|
566
|
+
**GA4** — Measurement Protocol `user_data`
|
|
567
|
+
([user-provided data](https://developers.google.com/analytics/devguides/collection/ga4/uid-data)),
|
|
568
|
+
built by `helpers/analytics.js` from the authenticated request's own user. Its rules are NOT
|
|
569
|
+
Meta's: `sha256_email_address` and `sha256_phone_number` (E.164 WITH the `+`), and an
|
|
570
|
+
`address` block whose `sha256_first_name` / `sha256_last_name` drop digits and symbols,
|
|
571
|
+
`sha256_street` keeps its digits, while `city`, `region` (the region NAME, not a code) and
|
|
572
|
+
`postal_code` ride in the CLEAR and `country` is UPPERCASE alpha-2. The address comes off the
|
|
573
|
+
account: `personal.location.street` and `personal.location.postalCode` are the account page's
|
|
574
|
+
optional fields ([#663](https://github.com/Omega-JS-Stack/omega/issues/663)). The account's own
|
|
575
|
+
`personal.location` wins over the request's geolocation, which stays the fallback.
|
|
576
|
+
|
|
577
|
+
**Where the signup's match data comes from.** The server half of `sign_up` fires from the
|
|
578
|
+
post-auth request (`routes/user/signup`, via `libraries/analytics/signup.js`), NOT from the
|
|
579
|
+
auth trigger: a trigger has no HTTP request behind it, so it could see no IP, no user agent,
|
|
580
|
+
no platform cookies and no attribution the browser had not posted yet. The browser posts the
|
|
581
|
+
cookies under `attribution.cookies` — the shape the checkout intent already sends — and the
|
|
582
|
+
route's own `flags.signupProcessed` gate keeps it to one fire per account. The dedupe id is
|
|
583
|
+
`sign_up.<uid>` on both halves, as before.
|
|
584
|
+
|
|
585
|
+
**Reading it back.** Two dev surfaces answer the two halves of "did this match?":
|
|
586
|
+
|
|
587
|
+
- the backend's fire log names, per provider, the keys that went and the accepted keys that
|
|
588
|
+
were empty — KEY NAMES ONLY, because a hashed email is still that person's email:
|
|
589
|
+
`deliverConversion: sign_up → meta blocked (dev) sent em,external_id,client_ip_address,client_user_agent; empty ph,fn,ln,… | tiktok …`
|
|
590
|
+
- the dev palette's **Ad match keys** section (the one dev surface,
|
|
591
|
+
[#342](https://github.com/Omega-JS-Stack/omega/issues/342)) shows the browser's side: the
|
|
592
|
+
consent state, which pixel scripts loaded, which platform cookies exist right now, and the
|
|
593
|
+
stored attribution's keys. The checkout intent's response echoes the cookie key names the
|
|
594
|
+
SERVER received, which is where a blocked pixel shows up.
|
|
595
|
+
|
|
596
|
+
## Verifying
|
|
597
|
+
|
|
598
|
+
- **The package**: `npm test` in `packages/analytics` (and the root `test:packages` lane) —
|
|
599
|
+
catalog shape, per-provider resolve, the guarded transport, the consent gate, the dev log.
|
|
600
|
+
- **A call site**: drive the flow with a dev server running and read the fire-log line —
|
|
601
|
+
that is the proof the event fired, with what, to whom. Reading the template proves nothing;
|
|
602
|
+
the catalog is what decides.
|
|
603
|
+
- **A live drive**: the platforms' own debuggers — GA4 DebugView, Meta Events Manager's Test
|
|
604
|
+
Events, the TikTok Pixel Helper — and both consent regions (an opt-in timezone must show
|
|
605
|
+
nothing loading before Accept).
|
|
606
|
+
|
|
607
|
+
## Enforcement
|
|
608
|
+
|
|
609
|
+
The checklist is the plugin's `omega:analytics` skill
|
|
610
|
+
([agent-plugins/claude/skills/analytics/SKILL.md](../../agent-plugins/claude/skills/analytics/SKILL.md)),
|
|
611
|
+
and the quality hook fires it on every surface that owns an event: the web flow pages and the
|
|
612
|
+
auth modules, the backend payment routes and the auth/webhook events, and the package itself.
|