@aglyn/aglyn 1.0.0-beta.217 → 1.0.0-beta.219
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/package.json +14 -14
- package/src/lib/app-utils/actions.d.ts +25 -374
- package/src/lib/app-utils/actions.js +101 -561
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/activity-presenter.d.ts +3 -3
- package/src/lib/app-utils/activity-presenter.js +36 -52
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/admin-audit-index.js +2 -2
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +11 -2
- package/src/lib/app-utils/analytics-events.js +11 -2
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/analytics-provider.d.ts +182 -0
- package/src/lib/app-utils/analytics-provider.js +133 -0
- package/src/lib/app-utils/analytics-provider.js.map +1 -0
- package/src/lib/app-utils/api-plugins.d.ts +46 -0
- package/src/lib/app-utils/api-plugins.js +19 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
- package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
- package/src/lib/app-utils/artifact-overrides.js.map +1 -0
- package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
- package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
- package/src/lib/app-utils/artifact-provenance.js.map +1 -0
- package/src/lib/app-utils/assist-credits.js +2 -1
- package/src/lib/app-utils/assist-credits.js.map +1 -1
- package/src/lib/app-utils/binding-token-catalog.d.ts +5 -13
- package/src/lib/app-utils/binding-token-catalog.js +17 -41
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/binding-tokens.js +1 -1
- package/src/lib/app-utils/binding-tokens.js.map +1 -1
- package/src/lib/app-utils/brand-support-line.d.ts +36 -0
- package/src/lib/app-utils/brand-support-line.js +38 -0
- package/src/lib/app-utils/brand-support-line.js.map +1 -0
- package/src/lib/app-utils/campaign-touch.d.ts +5 -5
- package/src/lib/app-utils/campaign-touch.js +8 -8
- package/src/lib/app-utils/campaign-touch.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +2 -2
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/consent-group-change.d.ts +4 -0
- package/src/lib/app-utils/consent-group-change.js.map +1 -1
- package/src/lib/app-utils/console-record-links.d.ts +10 -89
- package/src/lib/app-utils/console-record-links.js +3 -107
- package/src/lib/app-utils/console-record-links.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +16 -7
- package/src/lib/app-utils/console-routes.js +9 -3
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +23 -1
- package/src/lib/app-utils/contacts.js +21 -5
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/cross-pool-desync.d.ts +39 -0
- package/src/lib/app-utils/cross-pool-desync.js +47 -0
- package/src/lib/app-utils/cross-pool-desync.js.map +1 -0
- package/src/lib/app-utils/csv-upload.d.ts +29 -0
- package/src/lib/app-utils/csv-upload.js +29 -0
- package/src/lib/app-utils/csv-upload.js.map +1 -0
- package/src/lib/app-utils/dataset-models.d.ts +5 -5
- package/src/lib/app-utils/dataset-models.js +19 -2
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/datasets.d.ts +65 -11
- package/src/lib/app-utils/datasets.js +74 -11
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +6 -6
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +12 -6
- package/src/lib/app-utils/docs-help.generated.js +21 -6
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-index.generated.js +514 -318
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +3 -3
- package/src/lib/app-utils/docs-retrieval.js +14 -10
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/duplicate-resource.d.ts +4 -1
- package/src/lib/app-utils/duplicate-resource.js +7 -4
- package/src/lib/app-utils/duplicate-resource.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.d.ts +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/element-ui.d.ts +1 -1
- package/src/lib/app-utils/element-ui.js +1 -1
- package/src/lib/app-utils/element-ui.js.map +1 -1
- package/src/lib/app-utils/email-starter-policy.js +3 -3
- package/src/lib/app-utils/email-starter-policy.js.map +1 -1
- package/src/lib/app-utils/entity-picker-context.d.ts +10 -5
- package/src/lib/app-utils/entity-picker-context.js.map +1 -1
- package/src/lib/app-utils/error-beacon.d.ts +75 -6
- package/src/lib/app-utils/error-beacon.js +160 -21
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +10 -10
- package/src/lib/app-utils/expand-repeatables.js +10 -10
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/host-events.d.ts +82 -0
- package/src/lib/app-utils/host-events.js +62 -0
- package/src/lib/app-utils/host-events.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +33 -1
- package/src/lib/app-utils/lockdown.js +45 -4
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/margin-utilization.d.ts +4 -69
- package/src/lib/app-utils/margin-utilization.js +82 -69
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.d.ts +16 -16
- package/src/lib/app-utils/node-definition-sanitizer.js +24 -20
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/app-utils/node-interactions.d.ts +7 -7
- package/src/lib/app-utils/node-interactions.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +78 -47
- package/src/lib/app-utils/notifications.js +63 -44
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/payments-configured.d.ts +8 -21
- package/src/lib/app-utils/payments-configured.js +8 -23
- package/src/lib/app-utils/payments-configured.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +43 -128
- package/src/lib/app-utils/plan-entitlements.js +145 -304
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +10 -0
- package/src/lib/app-utils/platform-brand.js +14 -3
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/plugin-api-rate-limit.d.ts +7 -1
- package/src/lib/app-utils/plugin-api-rate-limit.js +11 -3
- package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.js +2 -2
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-host-events.generated.js +169 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -0
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js +106 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +14 -5
- package/src/lib/app-utils/release-flags.js +21 -125
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/repeat-sources.d.ts +9 -0
- package/src/lib/app-utils/repeat-sources.js.map +1 -1
- package/src/lib/app-utils/retained-refusals.d.ts +73 -0
- package/src/lib/app-utils/retained-refusals.js +186 -0
- package/src/lib/app-utils/retained-refusals.js.map +1 -0
- package/src/lib/app-utils/scope-tokens.d.ts +27 -0
- package/src/lib/app-utils/scope-tokens.js +61 -0
- package/src/lib/app-utils/scope-tokens.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +3 -3
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +15 -0
- package/src/lib/app-utils/screen-route.js +21 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +132 -0
- package/src/lib/app-utils/seo-audit.js +246 -0
- package/src/lib/app-utils/seo-audit.js.map +1 -0
- package/src/lib/app-utils/seo-keywords.d.ts +48 -0
- package/src/lib/app-utils/seo-keywords.js +60 -0
- package/src/lib/app-utils/seo-keywords.js.map +1 -0
- package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
- package/src/lib/app-utils/seo-page-facts.js +155 -0
- package/src/lib/app-utils/seo-page-facts.js.map +1 -0
- package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
- package/src/lib/app-utils/seo-site-scan.js +162 -0
- package/src/lib/app-utils/seo-site-scan.js.map +1 -0
- package/src/lib/app-utils/seo-title-variables.js +1 -1
- package/src/lib/app-utils/seo-title-variables.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +6 -9
- package/src/lib/app-utils/server.js +17 -27
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-interactions.d.ts +501 -0
- package/src/lib/app-utils/site-interactions.js +554 -0
- package/src/lib/app-utils/site-interactions.js.map +1 -0
- package/src/lib/app-utils/{marketplace-theme.d.ts → site-theme.d.ts} +13 -2
- package/src/lib/app-utils/{marketplace-theme.js → site-theme.js} +5 -2
- package/src/lib/app-utils/site-theme.js.map +1 -0
- package/src/lib/app-utils/sitemap.d.ts +4 -3
- package/src/lib/app-utils/sitemap.js +9 -7
- package/src/lib/app-utils/sitemap.js.map +1 -1
- package/src/lib/app-utils/stale-build-error.d.ts +51 -0
- package/src/lib/app-utils/stale-build-error.js +100 -1
- package/src/lib/app-utils/stale-build-error.js.map +1 -1
- package/src/lib/app-utils/starter-templates.d.ts +20 -0
- package/src/lib/app-utils/starter-templates.js +49 -0
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/tax-jurisdiction-figures.d.ts +118 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js +123 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js.map +1 -0
- package/src/lib/app-utils/theme-library.d.ts +10 -3
- package/src/lib/app-utils/theme-library.js +13 -4
- package/src/lib/app-utils/theme-library.js.map +1 -1
- package/src/lib/app-utils/{campaign-attribution.d.ts → utm-attribution.d.ts} +11 -6
- package/src/lib/app-utils/{campaign-attribution.js → utm-attribution.js} +13 -8
- package/src/lib/app-utils/utm-attribution.js.map +1 -0
- package/src/lib/app-utils/{campaign-forwarding.d.ts → utm-forwarding.d.ts} +14 -14
- package/src/lib/app-utils/{campaign-forwarding.js → utm-forwarding.js} +26 -26
- package/src/lib/app-utils/utm-forwarding.js.map +1 -0
- package/src/lib/app-utils/video-object.js +6 -5
- package/src/lib/app-utils/video-object.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +41 -164
- package/src/lib/app-utils/visitor-consent.js +66 -277
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/web-vitals-rum.d.ts +7 -7
- package/src/lib/app-utils/web-vitals-rum.js +31 -63
- package/src/lib/app-utils/web-vitals-rum.js.map +1 -1
- package/src/lib/app-utils/where-used.d.ts +11 -6
- package/src/lib/app-utils/where-used.js +3 -8
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/canvas-manager/canvas-manager.js +3 -3
- package/src/lib/canvas-manager/canvas-manager.js.map +1 -1
- package/src/lib/foundation/definitions/components.types.d.ts +1 -1
- package/src/lib/foundation/definitions/components.types.js +1 -1
- package/src/lib/foundation/definitions/components.types.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +18 -135
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +6 -3
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +27 -5
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
- package/src/lib/plugin-manager/besigner-documents.js +61 -0
- package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
- package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
- package/src/lib/plugin-manager/computed-variables.js +74 -0
- package/src/lib/plugin-manager/computed-variables.js.map +1 -0
- package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
- package/src/lib/plugin-manager/custom-fields.js +47 -1
- package/src/lib/plugin-manager/custom-fields.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +292 -20
- package/src/lib/plugin-manager/feature-plugins.js +105 -17
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +123 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +785 -14
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +25 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contributions.d.ts +1 -1
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-declarations-repair.d.ts +36 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js +64 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js.map +1 -0
- package/src/lib/plugin-manager/plugin-dependents.d.ts +101 -0
- package/src/lib/plugin-manager/plugin-dependents.js +75 -0
- package/src/lib/plugin-manager/plugin-dependents.js.map +1 -0
- package/src/lib/plugin-manager/plugin-distribution.d.ts +55 -0
- package/src/lib/plugin-manager/plugin-distribution.js +45 -0
- package/src/lib/plugin-manager/plugin-distribution.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +17 -0
- package/src/lib/plugin-manager/plugin-host-collections.js +6 -0
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-resources.d.ts +133 -0
- package/src/lib/plugin-manager/plugin-host-resources.js +52 -0
- package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +28 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js +61 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-plan-entitlements.d.ts +123 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js +101 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-index.d.ts +95 -0
- package/src/lib/plugin-manager/plugin-record-index.js +51 -0
- package/src/lib/plugin-manager/plugin-record-index.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-written.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-record-written.js +42 -0
- package/src/lib/plugin-manager/plugin-record-written.js.map +1 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.d.ts +213 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js +194 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-bundle.d.ts +145 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js +78 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-cache.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-export.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-export.js +40 -0
- package/src/lib/plugin-manager/plugin-site-export.js.map +1 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js +29 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tax-profile.d.ts +20 -4
- package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +255 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +191 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-template-sources.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-template-sources.js +37 -0
- package/src/lib/plugin-manager/plugin-template-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
- package/src/lib/plugin-manager/plugin-zones.d.ts +4 -5
- package/src/lib/plugin-manager/plugin-zones.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/record-captured-contact.d.ts +1 -14
- package/src/lib/plugin-manager/record-captured-contact.js +10 -32
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -1
- package/src/lib/plugin-manager/repeat-rows.d.ts +124 -0
- package/src/lib/plugin-manager/repeat-rows.js +108 -0
- package/src/lib/plugin-manager/repeat-rows.js.map +1 -0
- package/src/lib/plugin-manager/submission-record-target.d.ts +144 -0
- package/src/lib/plugin-manager/submission-record-target.js +125 -0
- package/src/lib/plugin-manager/submission-record-target.js.map +1 -0
- package/src/lib/plugin-manager/video-embed-provider.d.ts +108 -0
- package/src/lib/plugin-manager/video-embed-provider.js +165 -0
- package/src/lib/plugin-manager/video-embed-provider.js.map +1 -0
- package/src/lib/types/nodes.d.ts +2 -2
- package/src/lib/types/nodes.js.map +1 -1
- package/src/lib/app-utils/campaign-attribution.js.map +0 -1
- package/src/lib/app-utils/campaign-forwarding.js.map +0 -1
- package/src/lib/app-utils/contact-holder.d.ts +0 -68
- package/src/lib/app-utils/contact-holder.js +0 -129
- package/src/lib/app-utils/contact-holder.js.map +0 -1
- package/src/lib/app-utils/contact-merge.d.ts +0 -136
- package/src/lib/app-utils/contact-merge.js +0 -412
- package/src/lib/app-utils/contact-merge.js.map +0 -1
- package/src/lib/app-utils/crm-csv.d.ts +0 -239
- package/src/lib/app-utils/crm-csv.js +0 -467
- package/src/lib/app-utils/crm-csv.js.map +0 -1
- package/src/lib/app-utils/crm-digest.d.ts +0 -185
- package/src/lib/app-utils/crm-digest.js +0 -272
- package/src/lib/app-utils/crm-digest.js.map +0 -1
- package/src/lib/app-utils/form-dataset-binding.d.ts +0 -50
- package/src/lib/app-utils/form-dataset-binding.js +0 -74
- package/src/lib/app-utils/form-dataset-binding.js.map +0 -1
- package/src/lib/app-utils/list-import.d.ts +0 -158
- package/src/lib/app-utils/list-import.js +0 -332
- package/src/lib/app-utils/list-import.js.map +0 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +0 -122
- package/src/lib/app-utils/marketplace-listing-visibility.js +0 -147
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +0 -1
- package/src/lib/app-utils/marketplace-overrides.js.map +0 -1
- package/src/lib/app-utils/marketplace-provenance.js.map +0 -1
- package/src/lib/app-utils/marketplace-theme.js.map +0 -1
- package/src/lib/app-utils/marketplace-update-state.d.ts +0 -119
- package/src/lib/app-utils/marketplace-update-state.js +0 -132
- package/src/lib/app-utils/marketplace-update-state.js.map +0 -1
- package/src/lib/app-utils/publisher-agreement.d.ts +0 -289
- package/src/lib/app-utils/publisher-agreement.js +0 -321
- package/src/lib/app-utils/publisher-agreement.js.map +0 -1
- package/src/lib/app-utils/wistia-embed.d.ts +0 -104
- package/src/lib/app-utils/wistia-embed.js +0 -138
- package/src/lib/app-utils/wistia-embed.js.map +0 -1
- package/src/lib/app-utils/workflows.d.ts +0 -136
- package/src/lib/app-utils/workflows.js +0 -346
- package/src/lib/app-utils/workflows.js.map +0 -1
- package/src/lib/plugin-manager/commerce-zone-props.d.ts +0 -172
- package/src/lib/plugin-manager/commerce-zone-props.js +0 -31
- package/src/lib/plugin-manager/commerce-zone-props.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/activity-presenter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Activity-feed presenter (AGL-810). The `hosts/{id}/activity` and\n// `orgs/{id}/activity` logs store a free-text `action` plus a `{ type, id,\n// name }` target, written from ~40 scattered call sites. The three renderers\n// (host card, host table, org card) used to each format entries their own\n// way — one dropped the target entirely, one printed the raw Firestore doc\n// id when no name was recorded. This module is the single place that turns a\n// stored entry into something a human reads, and into a deep link to the\n// thing that changed. It is deliberately dependency-free (only the string\n// route table) so plugin libs and the console app can both import it via\n// `@aglyn/aglyn/app-utils/activity-presenter`.\n\nimport { Route, buildRoute } from './console-routes'\nimport {\n CRM_RECORD_SECTIONS,\n crmRecordHref,\n crmSectionHref,\n type CrmRecordKind,\n} from './console-record-links'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\nimport { hostEventLabel } from './workflows'\nimport { duplicateActivityActionLabel } from './duplicate-resource'\nimport {\n pluginActivityActionLabel,\n pluginActivityGroupForAction,\n pluginActivityTargetNoun,\n} from '../plugin-manager/plugin-activity-actions'\n\n/** The stored `target` sub-object, read defensively (any field may be absent). */\nexport interface ActivityTargetLike {\n type?: string\n id?: string\n name?: string\n /** Present on screen saves so the deep link can hit the exact version. */\n versionId?: string\n}\n\n/** A stored activity document, read defensively. */\nexport interface ActivityEntryLike {\n action?: string\n target?: ActivityTargetLike | null\n // Some early org entries carried the type/target id at the top level rather\n // than nested under `target` — tolerate both so no entry renders blank.\n type?: string\n targetId?: string\n /**\n * The acting uid: a person's, the literal `'api'` for a key, a\n * `system:` id for a platform process, or null when no person acted.\n */\n actorId?: string | null\n /** The address the actor had when the entry was written; null for a key. */\n actorEmail?: string | null\n /**\n * The address `actorId` holds TODAY, looked up on read by a route with the\n * Admin SDK, for an entry that recorded a uid and no address.\n */\n actorEmailNow?: string | null\n /**\n * For a STAFF reader only: the staff member behind an entry the workspace\n * did not perform (a staff cancellation), resolved by the route.\n */\n staffActorEmail?: string | null\n /** The name of the API key that wrote the entry, when a key did (AGL-2632). */\n apiKeyName?: string | null\n}\n\n/**\n * Who a host activity entry is attributed to — the writer's half of the\n * contract whose reader is {@link activityActorLabel}.\n *\n * A person is their verified uid and the address they had at the time. An\n * integration is the literal uid `'api'`, no address, and the name the\n * organization gave its key — carried because a feed that says \"Someone\"\n * about every write a key made is an audit trail with a hole in it: three\n * Zapier zaps and a nightly import all read as the same nobody.\n */\nexport interface HostActivityActor {\n uid: string\n email?: string | null\n /** The API key's name, for an entry an integration wrote. */\n apiKeyName?: string | null\n}\n\n/**\n * What the presenter needs to build a link. `host` is the `[host]` URL\n * segment — the site SLUG the console routes by (AGL-622), NOT the host doc\n * id — so pass `useParams().host`, not the doc id. When it is present the\n * entry is treated as host-scoped, otherwise org-scoped — which\n * disambiguates `member`, the one type both logs use.\n */\nexport interface ActivityLinkContext {\n orgSlug?: string\n host?: string\n}\n\n/** Human, singular noun for a target type. Falls back to the raw type. */\nconst TYPE_LABELS: Record<string, string> = {\n host: 'Site',\n screen: 'Screen',\n layout: 'Layout',\n theme: 'Theme',\n media: 'Media',\n content: 'Content',\n variable: 'Variable',\n function: 'Function',\n workflow: 'Workflow',\n member: 'Member',\n component: 'Component',\n template: 'Template',\n org: 'Organization',\n invite: 'Invitation',\n subscription: 'Subscription',\n // A custom role's AI permission flipped (AGL-2929).\n role: 'Role',\n // A generation job (AGL-2904): the org feed names the job, and each of its\n // outputs names the resource it produced.\n aiJob: 'AI generation',\n // CRM records (AGL-2622): a person added, a company or a deal created or\n // deleted, a lead converted — logged into the feed of the site whose\n // console did it, and linked to the record the way a screen is.\n contact: 'Contact',\n lead: 'Lead',\n company: 'Company',\n deal: 'Deal',\n}\n\n/** The activity target types that are CRM records, for the link switch. */\nconst CRM_TARGET_KINDS = new Set<string>(Object.keys(CRM_RECORD_SECTIONS))\n\n/**\n * The org-level hub's section for each record kind an org feed entry may\n * name (AGL-2634): the four record kinds, and tasks, which have a section\n * and no page of their own.\n */\nconst ORG_CRM_SECTIONS: Record<string, string> = {\n ...CRM_RECORD_SECTIONS,\n task: 'tasks',\n}\n\n/**\n * Human noun for a target type, e.g. `'screen'` → `'Screen'`. A plugin's\n * own resource is stored as `pluginId:noun` (AGL-2978), and a reader sees\n * the noun: `acme-mail:mailbox` → `Mailbox`.\n */\nexport function activityTypeLabel(type: string | undefined): string {\n if (!type) return 'Item'\n const known = TYPE_LABELS[type]\n if (known) return known\n const noun = pluginActivityTargetNoun(type) ?? type\n return noun.charAt(0).toUpperCase() + noun.slice(1)\n}\n\n/**\n * A label for the target column/link. Prefers the recorded name; when none\n * was written (older entries, id-only saves) it degrades to the humanized\n * type — NEVER the raw doc id, which is meaningless to a reader.\n */\nexport function activityTargetLabel(\n target: ActivityTargetLike | null | undefined,\n): string {\n if (target?.name) return target.name\n return activityTypeLabel(target?.type)\n}\n\n/**\n * What the action column reads (AGL-2929). Most actions are the sentence\n * their writer stored; a plugin's rows store a code (`ai.job.output`) so\n * that three surfaces can recognize them without parsing prose, and the\n * code is translated here — once, from the plugin-declared catalog\n * (AGL-2940) — rather than shown to a reader as a dotted path.\n */\nexport function activityActionLabel(action: string | undefined): string {\n const stored = action?.trim() ?? ''\n return (\n pluginActivityActionLabel(stored) ??\n duplicateActivityActionLabel(stored) ??\n stored\n )\n}\n\n/**\n * The filter group an entry's code belongs to — the chip that keeps it —\n * or `undefined` for a prose action no plugin declared.\n */\nexport function activityEntryGroupId(entry: ActivityEntryLike): string | undefined {\n return pluginActivityGroupForAction(entry.action?.trim())?.id\n}\n\n/**\n * The primary line for a feed entry: the action, suffixed with the target\n * name when one is known, so \"Saved the screen\" becomes \"Saved the screen —\n * Home\". A missing action degrades to the target label rather than blank.\n */\nexport function activityPrimaryText(entry: ActivityEntryLike): string {\n const action = activityActionLabel(entry.action)\n const name = entry.target?.name?.trim()\n if (action && name) return `${action} — ${name}`\n if (action) return action\n return activityTargetLabel(entry.target)\n}\n\n/** How an integration's key is named wherever an actor is: `API key Zapier`. */\nexport function apiKeyActorLabel(name: string): string {\n return `API key ${name}`\n}\n\n/**\n * The \"who\" line for an entry — always someone a reader can narrow down.\n *\n * In order: the address recorded when the entry was written; the key that\n * wrote it, by name; the address the recorded uid holds now, when the writer\n * held a uid and no address (the billing webhook, before it carried the\n * address stamped at the console act); else the uid itself, for an account\n * that no longer resolves.\n *\n * An entry with no uid at all is not an anonymous one. Every writer that\n * leaves `actorId` null is a platform process, so the feed names it: a\n * workflow run by its workflow, anything else — the billing webhook,\n * inbound CRM mail, a background AI job, a staff cancellation — as the\n * platform; a staff reader is also told which staff member. Never \"Someone\", which read as a person nobody could identify\n * (AGL-3369).\n */\nexport function activityActorLabel(entry: ActivityEntryLike): string {\n if (entry.actorEmail) return entry.actorEmail\n if (entry.apiKeyName) return apiKeyActorLabel(entry.apiKeyName)\n const actorId = entry.actorId?.trim()\n if (actorId === 'api') return 'API key'\n if (actorId && !actorId.startsWith('system:')) {\n return entry.actorEmailNow || `Account ${actorId}`\n }\n const target = entry.target ?? undefined\n if ((target?.type ?? entry.type) === 'workflow') {\n const name = target?.name?.trim()\n return name ? `Workflow ${name}` : 'Workflow'\n }\n if (entry.staffActorEmail) {\n return `${PLATFORM_BRAND_NAME} staff (${entry.staffActorEmail})`\n }\n return PLATFORM_BRAND_NAME\n}\n\n/**\n * Who set a run off (AGL-3376), as a run row stores it: the actor the event's\n * door named, carried through the engine. `webhook` is an inbound webhook's\n * caller, named by the hook.\n */\nexport interface RunTriggeredByLike {\n kind?: string\n uid?: string\n email?: string\n apiKeyName?: string\n name?: string\n}\n\n/**\n * The run history's Who: whoever set the run off, never the automation\n * itself — every row in a run history is that automation, so naming it\n * again would answer nothing. A run written before its trigger was recorded\n * says so rather than guessing from the event's name.\n */\nexport function runTriggeredByLabel(entry: {\n triggeredBy?: RunTriggeredByLike | null\n}): string {\n const by = entry.triggeredBy\n switch (by?.kind) {\n case 'member':\n return by.email || (by.uid ? `Account ${by.uid}` : 'A member')\n case 'apiKey':\n return by.apiKeyName ? apiKeyActorLabel(by.apiKeyName) : 'API key'\n case 'visitor':\n return by.email ? `Site visitor (${by.email})` : 'Site visitor'\n case 'webhook':\n return by.name ? `Inbound webhook ${by.name}` : 'Inbound webhook'\n case 'platform':\n return PLATFORM_BRAND_NAME\n default:\n return 'Not recorded'\n }\n}\n\n/**\n * A deep link to the resource an entry points at, or `undefined` when it\n * cannot be built (missing context, or a type with nowhere sensible to go).\n * `buildRoute` emits `<orgSlug?>` placeholders for missing params, so every\n * branch guards its inputs and returns `undefined` rather than a dead link.\n */\nexport function activityHref(\n entry: ActivityEntryLike,\n ctx: ActivityLinkContext,\n): string | undefined {\n const { orgSlug, host } = ctx\n if (!orgSlug) return undefined\n const target = entry.target ?? undefined\n const type = target?.type ?? entry.type\n const id = target?.id ?? entry.targetId\n const versionId = target?.versionId\n\n // Host-scoped entries: every host route needs both orgSlug and host.\n if (host) {\n switch (type) {\n case 'host':\n return buildRoute(Route.HOST_DASHBOARD, { orgSlug, host })\n case 'screen':\n // The screen detail page is version-addressed; without a versionId\n // there is no \"current\" pointer to route through, so land on the list.\n return id && versionId\n ? buildRoute(Route.SCREEN_DETAILS, {\n orgSlug,\n host,\n screenId: id,\n versionId,\n })\n : buildRoute(Route.HOST_SCREENS, { orgSlug, host })\n case 'component':\n return id\n ? buildRoute(Route.COMPONENT_DETAILS, {\n orgSlug,\n host,\n componentId: id,\n })\n : buildRoute(Route.HOST_COMPONENTS, { orgSlug, host })\n case 'template':\n return id\n ? buildRoute(Route.TEMPLATE_DETAILS, {\n orgSlug,\n host,\n templateId: id,\n })\n : buildRoute(Route.HOST_TEMPLATES, { orgSlug, host })\n case 'layout':\n return id\n ? buildRoute(Route.LAYOUT_DETAILS, { orgSlug, host, layoutId: id })\n : buildRoute(Route.HOST_LAYOUTS, { orgSlug, host })\n case 'theme':\n return buildRoute(Route.HOST_SETUP_THEME, { orgSlug, host })\n case 'media':\n return buildRoute(Route.HOST_MEDIA, { orgSlug, host })\n case 'content':\n return buildRoute(Route.HOST_CONTENT, { orgSlug, host })\n case 'variable':\n return buildRoute(Route.HOST_DATA, { orgSlug, host })\n case 'function':\n return buildRoute(Route.HOST_LOGIC, { orgSlug, host })\n case 'workflow':\n return buildRoute(Route.HOST_AUTOMATION, { orgSlug, host })\n case 'member':\n // Site team is managed on the Setup page — there is no per-member\n // host route.\n return buildRoute(Route.HOST_SETUP, { orgSlug, host })\n default:\n // A CRM record's own page, or its list when the entry named no id —\n // a deleted record still has a list to land on. The CRM is a plugin\n // hub, so its addresses come from the shared builder rather than\n // the route table, the same door the console's search uses.\n if (type && CRM_TARGET_KINDS.has(type)) {\n const kind = type as CrmRecordKind\n return id\n ? crmRecordHref({ orgSlug, host }, kind, id)\n : crmSectionHref({ orgSlug, host }, CRM_RECORD_SECTIONS[kind])\n }\n return undefined\n }\n }\n\n // Org-scoped entries.\n switch (type) {\n case 'org':\n return buildRoute(Route.ORG_SETTINGS, { orgSlug })\n case 'member':\n return id\n ? buildRoute(Route.MANAGE_TEAM_MEMBER, { orgSlug, uid: id })\n : buildRoute(Route.MANAGE_TEAM, { orgSlug })\n case 'invite':\n return buildRoute(Route.MANAGE_TEAM, { orgSlug })\n default: {\n // A CRM record acted on at the ORGANIZATION level (AGL-2634) — a deal\n // moved from the org board, a merge, a bulk bar's action — opens in\n // the org-level hub. A lead's org address needs its site, which the\n // entry does not carry, and a bulk line names no record: both land\n // on the section.\n const section = type ? ORG_CRM_SECTIONS[type] : undefined\n if (!section) return undefined\n const hub = `${buildRoute(Route.ORG_CRM, { orgSlug })}/${section}`\n return id && type !== 'lead' && type !== 'task'\n ? `${hub}/${encodeURIComponent(id)}`\n : hub\n }\n }\n}\n\n/**\n * How an automation run ended (AGL-2171).\n *\n * `skipped` is a first-class outcome, not an absence. An unmet condition\n * used to be a bare `continue` that wrote nothing, so \"why didn't my\n * automation fire?\" — the most common support question about automations —\n * had no answer anywhere in the product, while `/product/workflows`\n * advertises an amber `Skipped` row that answers it.\n */\nexport type ActionRunResult = 'succeeded' | 'failed' | 'skipped'\n\n/** A stored run entry: an activity document with the run fields on it. */\nexport interface ActionRunEntryLike extends ActivityEntryLike {\n result?: string\n trigger?: string\n summary?: string\n}\n\n/**\n * `formSubmission` → `Form submitted`.\n *\n * The console rendered the raw camelCase identifier in the trigger select\n * and in every run row. The words are `HOST_EVENT_LABELS` in `workflows.ts`\n * — one map, so the picker an author chose from and the run row they read\n * afterwards cannot name the same event differently.\n */\nexport function actionTriggerLabel(event: string | undefined): string {\n return hostEventLabel(event)\n}\n\n/**\n * The run's outcome, tolerating rows written before it was recorded.\n *\n * A legacy entry carries only the prose `action` string, so it is read\n * back: `with errors:` means it failed and `Action ran on` means it\n * succeeded. Anything else is not a run at all — the activity collection\n * also holds publishes, media saves and member changes, and calling one of\n * those `succeeded` would put it in the run history under a verdict that\n * means nothing for it.\n */\nexport function actionRunResult(\n entry: ActionRunEntryLike,\n): ActionRunResult | undefined {\n const stored = String(entry.result ?? '').trim()\n if (stored === 'succeeded' || stored === 'failed' || stored === 'skipped') {\n return stored\n }\n const action = String(entry.action ?? '')\n if (!action.startsWith('Action ran on')) return undefined\n return action.includes('with errors:') ? 'failed' : 'succeeded'\n}\n\n/**\n * The `What happened` column: `Sent email · saved to Leads · webhook 200`.\n *\n * Falls back to the legacy prose, minus its `Action ran on <event>` prefix\n * — that fact is the Trigger column now, and repeating it in the summary\n * would make every historic row say the same thing twice.\n */\nexport function actionRunSummary(entry: ActionRunEntryLike): string {\n const summary = String(entry.summary ?? '').trim()\n if (summary) return summary\n const action = String(entry.action ?? '').trim()\n const errors = action.match(/with errors:\\s*(.+)$/)\n if (errors) return errors[1]\n // A pre-AGL-2171 success recorded nothing but the fact that it ran.\n if (action.startsWith('Action ran on')) return 'Ran'\n return action\n}\n"],"names":["Route","buildRoute","CRM_RECORD_SECTIONS","crmRecordHref","crmSectionHref","PLATFORM_BRAND_NAME","hostEventLabel","duplicateActivityActionLabel","pluginActivityActionLabel","pluginActivityGroupForAction","pluginActivityTargetNoun","TYPE_LABELS","host","screen","layout","theme","media","content","variable","function","workflow","member","component","template","org","invite","subscription","role","aiJob","contact","lead","company","deal","CRM_TARGET_KINDS","Set","Object","keys","ORG_CRM_SECTIONS","task","activityTypeLabel","type","known","noun","charAt","toUpperCase","slice","activityTargetLabel","target","name","activityActionLabel","action","stored","trim","activityEntryGroupId","entry","id","activityPrimaryText","apiKeyActorLabel","activityActorLabel","actorEmail","apiKeyName","actorId","startsWith","actorEmailNow","undefined","staffActorEmail","runTriggeredByLabel","by","triggeredBy","kind","email","uid","activityHref","ctx","orgSlug","targetId","versionId","HOST_DASHBOARD","SCREEN_DETAILS","screenId","HOST_SCREENS","COMPONENT_DETAILS","componentId","HOST_COMPONENTS","TEMPLATE_DETAILS","templateId","HOST_TEMPLATES","LAYOUT_DETAILS","layoutId","HOST_LAYOUTS","HOST_SETUP_THEME","HOST_MEDIA","HOST_CONTENT","HOST_DATA","HOST_LOGIC","HOST_AUTOMATION","HOST_SETUP","has","ORG_SETTINGS","MANAGE_TEAM_MEMBER","MANAGE_TEAM","section","hub","ORG_CRM","encodeURIComponent","actionTriggerLabel","event","actionRunResult","String","result","includes","actionRunSummary","summary","errors","match"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/activity-presenter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Activity-feed presenter (AGL-810). The `hosts/{id}/activity` and\n// `orgs/{id}/activity` logs store a free-text `action` plus a `{ type, id,\n// name }` target, written from ~40 scattered call sites. The three renderers\n// (host card, host table, org card) used to each format entries their own\n// way — one dropped the target entirely, one printed the raw Firestore doc\n// id when no name was recorded. This module is the single place that turns a\n// stored entry into something a human reads, and into a deep link to the\n// thing that changed. It is deliberately light (the string route table and\n// the record-route registry) so plugin libs and the console app can both\n// import it via `@aglyn/aglyn/app-utils/activity-presenter`.\n\nimport { Route, buildRoute } from './console-routes'\nimport {\n pluginRecordHref,\n pluginRecordListHref,\n type PluginRecordRouteContext,\n} from '../plugin-manager/plugin-record-routes'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\nimport { hostEventLabel } from './host-events'\nimport { duplicateActivityActionLabel } from './duplicate-resource'\nimport {\n pluginActivityActionLabel,\n pluginActivityGroupForAction,\n pluginActivityTargetLabel,\n pluginActivityTargetNoun,\n} from '../plugin-manager/plugin-activity-actions'\n\n/** The stored `target` sub-object, read defensively (any field may be absent). */\nexport interface ActivityTargetLike {\n type?: string\n id?: string\n name?: string\n /** Present on screen saves so the deep link can hit the exact version. */\n versionId?: string\n}\n\n/** A stored activity document, read defensively. */\nexport interface ActivityEntryLike {\n action?: string\n target?: ActivityTargetLike | null\n // Some early org entries carried the type/target id at the top level rather\n // than nested under `target` — tolerate both so no entry renders blank.\n type?: string\n targetId?: string\n /**\n * The acting uid: a person's, the literal `'api'` for a key, a\n * `system:` id for a platform process, or null when no person acted.\n */\n actorId?: string | null\n /** The address the actor had when the entry was written; null for a key. */\n actorEmail?: string | null\n /**\n * The address `actorId` holds TODAY, looked up on read by a route with the\n * Admin SDK, for an entry that recorded a uid and no address.\n */\n actorEmailNow?: string | null\n /**\n * For a STAFF reader only: the staff member behind an entry the workspace\n * did not perform (a staff cancellation), resolved by the route.\n */\n staffActorEmail?: string | null\n /** The name of the API key that wrote the entry, when a key did (AGL-2632). */\n apiKeyName?: string | null\n}\n\n/**\n * Who a host activity entry is attributed to — the writer's half of the\n * contract whose reader is {@link activityActorLabel}.\n *\n * A person is their verified uid and the address they had at the time. An\n * integration is the literal uid `'api'`, no address, and the name the\n * organization gave its key — carried because a feed that says \"Someone\"\n * about every write a key made is an audit trail with a hole in it: three\n * Zapier zaps and a nightly import all read as the same nobody.\n */\nexport interface HostActivityActor {\n uid: string\n email?: string | null\n /** The API key's name, for an entry an integration wrote. */\n apiKeyName?: string | null\n}\n\n/**\n * What the presenter needs to build a link. `host` is the `[host]` URL\n * segment — the site SLUG the console routes by (AGL-622), NOT the host doc\n * id — so pass `useParams().host`, not the doc id. When it is present the\n * entry is treated as host-scoped, otherwise org-scoped — which\n * disambiguates `member`, the one type both logs use.\n */\nexport interface ActivityLinkContext {\n orgSlug?: string\n host?: string\n}\n\n/** Human, singular noun for a target type. Falls back to the raw type. */\nconst TYPE_LABELS: Record<string, string> = {\n host: 'Site',\n screen: 'Page',\n layout: 'Layout',\n theme: 'Theme',\n media: 'Media',\n content: 'Content',\n variable: 'Variable',\n function: 'Function',\n workflow: 'Workflow',\n member: 'Member',\n component: 'Component',\n template: 'Template',\n org: 'Organization',\n invite: 'Invitation',\n subscription: 'Subscription',\n // A custom role's permission flipped (AGL-2929).\n role: 'Role',\n // CRM records (AGL-2622): a person added, a company or a deal created or\n // deleted, a lead converted — logged into the feed of the site whose\n // console did it, and linked to the record the way a screen is.\n contact: 'Contact',\n lead: 'Lead',\n company: 'Company',\n deal: 'Deal',\n}\n\n/**\n * A plugin's record, where its owner publishes an address for the kind\n * (`plugin-record-routes`): its own page, or the list it lives on when the\n * entry named no id — a deleted record still has a list to land on.\n * `undefined` for a kind no plugin publishes here.\n */\nfunction pluginRecordActivityHref(\n type: string | undefined,\n id: string | undefined,\n context: PluginRecordRouteContext,\n): string | undefined {\n if (!type) return undefined\n return (id ? pluginRecordHref(type, context, id) : pluginRecordListHref(type, context)) ?? undefined\n}\n\n/**\n * Human noun for a target type, e.g. `'screen'` → `'Screen'`. A plugin's\n * own resource is stored as `pluginId:noun` (AGL-2978), and a reader sees\n * the noun: `acme-mail:mailbox` → `Mailbox`.\n */\nexport function activityTypeLabel(type: string | undefined): string {\n if (!type) return 'Item'\n const known = TYPE_LABELS[type] ?? pluginActivityTargetLabel(type)\n if (known) return known\n const noun = pluginActivityTargetNoun(type) ?? type\n return noun.charAt(0).toUpperCase() + noun.slice(1)\n}\n\n/**\n * A label for the target column/link. Prefers the recorded name; when none\n * was written (older entries, id-only saves) it degrades to the humanized\n * type — NEVER the raw doc id, which is meaningless to a reader.\n */\nexport function activityTargetLabel(\n target: ActivityTargetLike | null | undefined,\n): string {\n if (target?.name) return target.name\n return activityTypeLabel(target?.type)\n}\n\n/**\n * What the action column reads (AGL-2929). Most actions are the sentence\n * their writer stored; a plugin's rows store a code (`ai.job.output`) so\n * that three surfaces can recognize them without parsing prose, and the\n * code is translated here — once, from the plugin-declared catalog\n * (AGL-2940) — rather than shown to a reader as a dotted path.\n */\nexport function activityActionLabel(action: string | undefined): string {\n const stored = action?.trim() ?? ''\n return (\n pluginActivityActionLabel(stored) ??\n duplicateActivityActionLabel(stored) ??\n stored\n )\n}\n\n/**\n * The filter group an entry's code belongs to — the chip that keeps it —\n * or `undefined` for a prose action no plugin declared.\n */\nexport function activityEntryGroupId(entry: ActivityEntryLike): string | undefined {\n return pluginActivityGroupForAction(entry.action?.trim())?.id\n}\n\n/**\n * The primary line for a feed entry: the action, suffixed with the target\n * name when one is known, so \"Saved the screen\" becomes \"Saved the screen —\n * Home\". A missing action degrades to the target label rather than blank.\n */\nexport function activityPrimaryText(entry: ActivityEntryLike): string {\n const action = activityActionLabel(entry.action)\n const name = entry.target?.name?.trim()\n if (action && name) return `${action} — ${name}`\n if (action) return action\n return activityTargetLabel(entry.target)\n}\n\n/** How an integration's key is named wherever an actor is: `API key Zapier`. */\nexport function apiKeyActorLabel(name: string): string {\n return `API key ${name}`\n}\n\n/**\n * The \"who\" line for an entry — always someone a reader can narrow down.\n *\n * In order: the address recorded when the entry was written; the key that\n * wrote it, by name; the address the recorded uid holds now, when the writer\n * held a uid and no address (the billing webhook, before it carried the\n * address stamped at the console act); else the uid itself, for an account\n * that no longer resolves.\n *\n * An entry with no uid at all is not an anonymous one. Every writer that\n * leaves `actorId` null is a platform process, so the feed names it: a\n * workflow run by its workflow, anything else — the billing webhook,\n * inbound CRM mail, a background AI job, a staff cancellation — as the\n * platform; a staff reader is also told which staff member. Never \"Someone\", which read as a person nobody could identify\n * (AGL-3369).\n */\nexport function activityActorLabel(entry: ActivityEntryLike): string {\n if (entry.actorEmail) return entry.actorEmail\n if (entry.apiKeyName) return apiKeyActorLabel(entry.apiKeyName)\n const actorId = entry.actorId?.trim()\n if (actorId === 'api') return 'API key'\n if (actorId && !actorId.startsWith('system:')) {\n return entry.actorEmailNow || `Account ${actorId}`\n }\n const target = entry.target ?? undefined\n if ((target?.type ?? entry.type) === 'workflow') {\n const name = target?.name?.trim()\n return name ? `Workflow ${name}` : 'Workflow'\n }\n if (entry.staffActorEmail) {\n return `${PLATFORM_BRAND_NAME} staff (${entry.staffActorEmail})`\n }\n return PLATFORM_BRAND_NAME\n}\n\n/**\n * Who set a run off (AGL-3376), as a run row stores it: the actor the event's\n * door named, carried through the engine. `webhook` is an inbound webhook's\n * caller, named by the hook.\n */\nexport interface RunTriggeredByLike {\n kind?: string\n uid?: string\n email?: string\n apiKeyName?: string\n name?: string\n}\n\n/**\n * The run history's Who: whoever set the run off, never the automation\n * itself — every row in a run history is that automation, so naming it\n * again would answer nothing. A run written before its trigger was recorded\n * says so rather than guessing from the event's name.\n */\nexport function runTriggeredByLabel(entry: {\n triggeredBy?: RunTriggeredByLike | null\n}): string {\n const by = entry.triggeredBy\n switch (by?.kind) {\n case 'member':\n return by.email || (by.uid ? `Account ${by.uid}` : 'A member')\n case 'apiKey':\n return by.apiKeyName ? apiKeyActorLabel(by.apiKeyName) : 'API key'\n case 'visitor':\n return by.email ? `Site visitor (${by.email})` : 'Site visitor'\n case 'webhook':\n return by.name ? `Inbound webhook ${by.name}` : 'Inbound webhook'\n case 'platform':\n return PLATFORM_BRAND_NAME\n default:\n return 'Not recorded'\n }\n}\n\n/**\n * A deep link to the resource an entry points at, or `undefined` when it\n * cannot be built (missing context, or a type with nowhere sensible to go).\n * `buildRoute` emits `<orgSlug?>` placeholders for missing params, so every\n * branch guards its inputs and returns `undefined` rather than a dead link.\n */\nexport function activityHref(\n entry: ActivityEntryLike,\n ctx: ActivityLinkContext,\n): string | undefined {\n const { orgSlug, host } = ctx\n if (!orgSlug) return undefined\n const target = entry.target ?? undefined\n const type = target?.type ?? entry.type\n const id = target?.id ?? entry.targetId\n const versionId = target?.versionId\n\n // Host-scoped entries: every host route needs both orgSlug and host.\n if (host) {\n switch (type) {\n case 'host':\n return buildRoute(Route.HOST_DASHBOARD, { orgSlug, host })\n case 'screen':\n // The screen detail page is version-addressed; without a versionId\n // there is no \"current\" pointer to route through, so land on the list.\n return id && versionId\n ? buildRoute(Route.SCREEN_DETAILS, {\n orgSlug,\n host,\n screenId: id,\n versionId,\n })\n : buildRoute(Route.HOST_SCREENS, { orgSlug, host })\n case 'component':\n return id\n ? buildRoute(Route.COMPONENT_DETAILS, {\n orgSlug,\n host,\n componentId: id,\n })\n : buildRoute(Route.HOST_COMPONENTS, { orgSlug, host })\n case 'template':\n return id\n ? buildRoute(Route.TEMPLATE_DETAILS, {\n orgSlug,\n host,\n templateId: id,\n })\n : buildRoute(Route.HOST_TEMPLATES, { orgSlug, host })\n case 'layout':\n return id\n ? buildRoute(Route.LAYOUT_DETAILS, { orgSlug, host, layoutId: id })\n : buildRoute(Route.HOST_LAYOUTS, { orgSlug, host })\n case 'theme':\n return buildRoute(Route.HOST_SETUP_THEME, { orgSlug, host })\n case 'media':\n return buildRoute(Route.HOST_MEDIA, { orgSlug, host })\n case 'content':\n return buildRoute(Route.HOST_CONTENT, { orgSlug, host })\n case 'variable':\n return buildRoute(Route.HOST_DATA, { orgSlug, host })\n case 'function':\n return buildRoute(Route.HOST_LOGIC, { orgSlug, host })\n case 'workflow':\n return buildRoute(Route.HOST_AUTOMATION, { orgSlug, host })\n case 'member':\n // Site team is managed on the Setup page — there is no per-member\n // host route.\n return buildRoute(Route.HOST_SETUP, { orgSlug, host })\n default:\n // A plugin's record — a contact, a deal, an order — at the address\n // its owner publishes, which the route table does not hold.\n return pluginRecordActivityHref(type, id, { orgSlug, host })\n }\n }\n\n // Org-scoped entries.\n switch (type) {\n case 'org':\n return buildRoute(Route.ORG_SETTINGS, { orgSlug })\n case 'member':\n return id\n ? buildRoute(Route.MANAGE_TEAM_MEMBER, { orgSlug, uid: id })\n : buildRoute(Route.MANAGE_TEAM, { orgSlug })\n case 'invite':\n return buildRoute(Route.MANAGE_TEAM, { orgSlug })\n default:\n // A plugin's record acted on at the ORGANIZATION level (AGL-2634) — a\n // deal moved from the org board, a merge, a bulk bar's action — at the\n // organization address its owner publishes; a bulk line names no\n // record and lands on the list.\n return pluginRecordActivityHref(type, id, { orgSlug, host: null })\n }\n}\n\n/**\n * How an automation run ended (AGL-2171).\n *\n * `skipped` is a first-class outcome, not an absence. An unmet condition\n * used to be a bare `continue` that wrote nothing, so \"why didn't my\n * automation fire?\" — the most common support question about automations —\n * had no answer anywhere in the product, while `/product/workflows`\n * advertises an amber `Skipped` row that answers it.\n */\nexport type ActionRunResult = 'succeeded' | 'failed' | 'skipped'\n\n/** A stored run entry: an activity document with the run fields on it. */\nexport interface ActionRunEntryLike extends ActivityEntryLike {\n result?: string\n trigger?: string\n summary?: string\n}\n\n/**\n * `formSubmission` → `Form submitted`.\n *\n * The console rendered the raw camelCase identifier in the trigger select\n * and in every run row. The words are each event's declared label\n * (`host-events.ts`) — one map, so the picker an author chose from and the\n * run row they read afterwards cannot name the same event differently.\n */\nexport function actionTriggerLabel(event: string | undefined): string {\n return hostEventLabel(event)\n}\n\n/**\n * The run's outcome, tolerating rows written before it was recorded.\n *\n * A legacy entry carries only the prose `action` string, so it is read\n * back: `with errors:` means it failed and `Action ran on` means it\n * succeeded. Anything else is not a run at all — the activity collection\n * also holds publishes, media saves and member changes, and calling one of\n * those `succeeded` would put it in the run history under a verdict that\n * means nothing for it.\n */\nexport function actionRunResult(\n entry: ActionRunEntryLike,\n): ActionRunResult | undefined {\n const stored = String(entry.result ?? '').trim()\n if (stored === 'succeeded' || stored === 'failed' || stored === 'skipped') {\n return stored\n }\n const action = String(entry.action ?? '')\n if (!action.startsWith('Action ran on')) return undefined\n return action.includes('with errors:') ? 'failed' : 'succeeded'\n}\n\n/**\n * The `What happened` column: `Sent email · saved to Leads · webhook 200`.\n *\n * Falls back to the legacy prose, minus its `Action ran on <event>` prefix\n * — that fact is the Trigger column now, and repeating it in the summary\n * would make every historic row say the same thing twice.\n */\nexport function actionRunSummary(entry: ActionRunEntryLike): string {\n const summary = String(entry.summary ?? '').trim()\n if (summary) return summary\n const action = String(entry.action ?? '').trim()\n const errors = action.match(/with errors:\\s*(.+)$/)\n if (errors) return errors[1]\n // A pre-AGL-2171 success recorded nothing but the fact that it ran.\n if (action.startsWith('Action ran on')) return 'Ran'\n return action\n}\n"],"names":["Route","buildRoute","pluginRecordHref","pluginRecordListHref","PLATFORM_BRAND_NAME","hostEventLabel","duplicateActivityActionLabel","pluginActivityActionLabel","pluginActivityGroupForAction","pluginActivityTargetLabel","pluginActivityTargetNoun","TYPE_LABELS","host","screen","layout","theme","media","content","variable","function","workflow","member","component","template","org","invite","subscription","role","contact","lead","company","deal","pluginRecordActivityHref","type","id","context","undefined","activityTypeLabel","known","noun","charAt","toUpperCase","slice","activityTargetLabel","target","name","activityActionLabel","action","stored","trim","activityEntryGroupId","entry","activityPrimaryText","apiKeyActorLabel","activityActorLabel","actorEmail","apiKeyName","actorId","startsWith","actorEmailNow","staffActorEmail","runTriggeredByLabel","by","triggeredBy","kind","email","uid","activityHref","ctx","orgSlug","targetId","versionId","HOST_DASHBOARD","SCREEN_DETAILS","screenId","HOST_SCREENS","COMPONENT_DETAILS","componentId","HOST_COMPONENTS","TEMPLATE_DETAILS","templateId","HOST_TEMPLATES","LAYOUT_DETAILS","layoutId","HOST_LAYOUTS","HOST_SETUP_THEME","HOST_MEDIA","HOST_CONTENT","HOST_DATA","HOST_LOGIC","HOST_AUTOMATION","HOST_SETUP","ORG_SETTINGS","MANAGE_TEAM_MEMBER","MANAGE_TEAM","actionTriggerLabel","event","actionRunResult","String","result","includes","actionRunSummary","summary","errors","match"],"mappings":"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"}
|
|
@@ -90,8 +90,8 @@ import { nameSearchTokens } from "./name-search.js";
|
|
|
90
90
|
* the impersonations rather than beside the record views.
|
|
91
91
|
*/ export const ADMIN_AUDIT_ACCESS_ACTIONS = [
|
|
92
92
|
'email.message-viewed',
|
|
93
|
-
// The acquisition card (AGL-3289)
|
|
94
|
-
//
|
|
93
|
+
// The acquisition card (AGL-3289). No longer written; the rows already in
|
|
94
|
+
// the log still classify as reads.
|
|
95
95
|
'user.acquisition-viewed',
|
|
96
96
|
'org.acquisition-viewed'
|
|
97
97
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/admin-audit-index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isPluginStaffAuditAccess,\n pluginStaffAuditActionGroup,\n} from '../plugin-manager/plugin-activity-actions'\nimport { nameSearchTokens } from './name-search'\n\n/*\n * WHAT THE STAFF AUDIT LOG IS QUERIED BY, WRITTEN WITH EVERY ROW (AGL-3321).\n *\n * The staff audit page and the audit tables on a staff account's page filter\n * `adminAudit` by fields no writer used to store — the action's group, the\n * kind of thing acted on, the site, whether the act only looked — and search\n * it. All of that used to be answered by reading the log in batches and\n * keeping the rows that matched, and a match read that way stops wherever\n * the batches stop, so an entry past them was reported as not there. On an\n * audit trail that is the one wrong answer that matters.\n *\n * So each is stored on the row as it is written, and the query asks for it:\n *\n * actionGroup the facet's group for the action, answered by the plugin\n * activity registry (`pluginStaffAuditActionGroup`): a\n * registered group by code or by `staffAuditPrefixes`,\n * otherwise the action's leading namespace.\n * kind `access` for an act that only looked (a core read action,\n * or one a plugin declares in `staffAuditAccessActions`),\n * `change` for everything else.\n * targetKind the kind of record acted on: the first segment of the\n * target path (`orgs`, `users`, `hosts`, `lockdowns`).\n * targetHostId the site acted on, when the target is a site or a record\n * under one (`hosts/{id}/…`, `orgs/{org}/hosts/{id}/…`);\n * null otherwise, so \"no site\" is a value a query can ask for.\n * searchTokens word-prefix tokens (`nameSearchTokens`) of the fields a\n * reviewer searches by, for `array-contains` on one word.\n *\n * Every write goes through `withAdminAuditIndex`, on the server through\n * `addAdminAudit` / `setAdminAudit` / `recordAdminAudit` in\n * `@aglyn/tenant-data-admin`. `apps/console/specs/admin-audit-writes-are-stamped.spec.ts`\n * refuses a write to the collection anywhere else, and\n * `apps/console/specs/admin-audit-action-groups.spec.ts` pins the registry's\n * groups and reads to `tools/scripts/lib/admin-audit-index.fixtures.json`,\n * the mapping `tools/scripts/backfill-admin-audit-index.mjs` restamps old\n * rows with. A plugin adding a code, a prefix or a read action therefore\n * fails CI until the fixture names it, and the backfill is re-run.\n */\n\n/** The row's group, as the Action group filter asks for it. */\nexport const ADMIN_AUDIT_GROUP_FIELD = 'actionGroup'\n\n/** Whether the row only looked, as the account page's two tables ask for it. */\nexport const ADMIN_AUDIT_KIND_FIELD = 'kind'\n\n/** The kind of record acted on, as the Target type filter asks for it. */\nexport const ADMIN_AUDIT_TARGET_KIND_FIELD = 'targetKind'\n\n/** The site acted on, as the Site filter asks for it. */\nexport const ADMIN_AUDIT_SITE_FIELD = 'targetHostId'\n\n/** The row's search tokens, as the search asks for one. */\nexport const ADMIN_AUDIT_SEARCH_FIELD = 'searchTokens'\n\n/**\n * The fields a search reaches, in the order they claim the token budget:\n * what was done, who did it, what it was done to, then why.\n */\nexport const ADMIN_AUDIT_SEARCHED_FIELDS = [\n 'action',\n 'actorEmail',\n 'target',\n 'actorUid',\n 'scope',\n 'reason',\n 'note',\n] as const\n\n/**\n * The most tokens one row stores. A free-text note is the only field that\n * can run long, and it is searched last, so it is the one that loses reach\n * past the cap.\n */\nexport const ADMIN_AUDIT_SEARCH_TOKEN_LIMIT = 200\n\n/** Access looked at data; change altered something or acted on someone. */\nexport type AdminAuditKind = 'access' | 'change'\n\n/**\n * The core actions that only LOOKED.\n *\n * An exception list, not a classification of everything, and the default\n * matters more than the membership: anything absent is a `change`. A change\n * is the louder half of the console's audit card, so an action nobody has\n * classified yet gets the MORE prominent treatment rather than the quieter\n * one. The failure mode of the opposite default is an unclassified\n * impersonation rendering as routine browsing.\n *\n * An export is deliberately NOT here. Data leaving the platform is a\n * high-consequence act even though it mutates nothing, and it belongs beside\n * the impersonations rather than beside the record views.\n */\nexport const ADMIN_AUDIT_ACCESS_ACTIONS: readonly string[] = [\n 'email.message-viewed',\n // The acquisition card (AGL-3289): where an account or a workspace came\n // from, cross-checked against the sales workspace's people. Read only.\n 'user.acquisition-viewed',\n 'org.acquisition-viewed',\n]\n\n/** What a row carries that the stamped fields are derived from. */\nexport type AdminAuditIndexSource = Partial<\n Record<(typeof ADMIN_AUDIT_SEARCHED_FIELDS)[number], unknown>\n>\n\n/** The fields the lists query. */\nexport interface AdminAuditIndexFields {\n actionGroup: string\n kind: AdminAuditKind\n targetKind: string\n targetHostId: string | null\n searchTokens: string[]\n}\n\n/** Separators inside a value: an address's `@` and `.`, a path's `/`, a code's `.`. */\nconst SEPARATORS = /[^\\p{L}\\p{N}]+/gu\n\n/**\n * The search tokens for one row.\n *\n * Each value is tokenized twice: as written, so a typed address or code\n * (`jane@acme`, `org.override`) matches from its start, and split at its\n * separators, so a reader finds `org.override` by `override` and an address\n * by its domain.\n */\nexport function adminAuditSearchTokens(entry: AdminAuditIndexSource): string[] {\n const tokens = new Set<string>()\n for (const field of ADMIN_AUDIT_SEARCHED_FIELDS) {\n const value = entry[field]\n if (typeof value !== 'string' || !value.trim()) continue\n const words = [\n ...nameSearchTokens(value),\n ...nameSearchTokens(value.replace(SEPARATORS, ' ')),\n ]\n for (const token of words) {\n tokens.add(token)\n if (tokens.size >= ADMIN_AUDIT_SEARCH_TOKEN_LIMIT) return [...tokens]\n }\n }\n return [...tokens]\n}\n\n/**\n * The group an action is filed under — the same answer the page's facet\n * offers. Empty for a row with no action.\n */\nexport function adminAuditActionGroup(action: unknown): string {\n return pluginStaffAuditActionGroup(action)\n}\n\n/**\n * An access when the platform or a plugin declares the action a read — a\n * plugin's staff card opening on an org or an account names its own read\n * actions through its activity group (AGL-2939) — and a change otherwise.\n */\nexport function adminAuditKind(action: unknown): AdminAuditKind {\n return typeof action === 'string' &&\n action &&\n (ADMIN_AUDIT_ACCESS_ACTIONS.includes(action) || isPluginStaffAuditAccess(action))\n ? 'access'\n : 'change'\n}\n\n/** The segments of a target path; empty for a target that is not one. */\nconst segmentsOf = (target: unknown): string[] =>\n typeof target === 'string' ? target.trim().split('/').filter(Boolean) : []\n\n/**\n * The kind of record acted on: the target's first segment, which for every\n * path-shaped target is its collection (`orgs/{id}` → `orgs`). A target\n * that is an identifier rather than a path (`sso-domains:acme.com`) is its\n * own type up to the first `:`. Empty for a row with no target.\n */\nexport function adminAuditTargetKind(target: unknown): string {\n const [first = ''] = segmentsOf(target)\n const colon = first.indexOf(':')\n return colon > 0 ? first.slice(0, colon) : first\n}\n\n/**\n * The site acted on: the id after a `hosts` segment in the target path —\n * `hosts/{id}` and anything under it, and a site filed under its\n * organization (`orgs/{org}/hosts/{id}`). Null when the act was not on a site.\n */\nexport function adminAuditTargetHostId(target: unknown): string | null {\n const segments = segmentsOf(target)\n for (let at = 0; at < segments.length - 1; at += 2) {\n if (segments[at] === 'hosts') return segments[at + 1] || null\n }\n return null\n}\n\n/** Every stamped field for one row. */\nexport function adminAuditIndexFields(\n entry: AdminAuditIndexSource,\n): AdminAuditIndexFields {\n return {\n actionGroup: adminAuditActionGroup(entry.action),\n kind: adminAuditKind(entry.action),\n targetKind: adminAuditTargetKind(entry.target),\n targetHostId: adminAuditTargetHostId(entry.target),\n searchTokens: adminAuditSearchTokens(entry),\n }\n}\n\n/**\n * The row as it is stored: the entry, with the fields its lists query.\n * Every write to `adminAudit` passes its data through this.\n */\nexport function withAdminAuditIndex<Entry extends AdminAuditIndexSource>(\n entry: Entry,\n): Entry & AdminAuditIndexFields {\n return { ...entry, ...adminAuditIndexFields(entry) }\n}\n"],"names":["isPluginStaffAuditAccess","pluginStaffAuditActionGroup","nameSearchTokens","ADMIN_AUDIT_GROUP_FIELD","ADMIN_AUDIT_KIND_FIELD","ADMIN_AUDIT_TARGET_KIND_FIELD","ADMIN_AUDIT_SITE_FIELD","ADMIN_AUDIT_SEARCH_FIELD","ADMIN_AUDIT_SEARCHED_FIELDS","ADMIN_AUDIT_SEARCH_TOKEN_LIMIT","ADMIN_AUDIT_ACCESS_ACTIONS","SEPARATORS","adminAuditSearchTokens","entry","tokens","Set","field","value","trim","words","replace","token","add","size","adminAuditActionGroup","action","adminAuditKind","includes","segmentsOf","target","split","filter","Boolean","adminAuditTargetKind","first","colon","indexOf","slice","adminAuditTargetHostId","segments","at","length","adminAuditIndexFields","actionGroup","kind","targetKind","targetHostId","searchTokens","withAdminAuditIndex"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/admin-audit-index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isPluginStaffAuditAccess,\n pluginStaffAuditActionGroup,\n} from '../plugin-manager/plugin-activity-actions'\nimport { nameSearchTokens } from './name-search'\n\n/*\n * WHAT THE STAFF AUDIT LOG IS QUERIED BY, WRITTEN WITH EVERY ROW (AGL-3321).\n *\n * The staff audit page and the audit tables on a staff account's page filter\n * `adminAudit` by fields no writer used to store — the action's group, the\n * kind of thing acted on, the site, whether the act only looked — and search\n * it. All of that used to be answered by reading the log in batches and\n * keeping the rows that matched, and a match read that way stops wherever\n * the batches stop, so an entry past them was reported as not there. On an\n * audit trail that is the one wrong answer that matters.\n *\n * So each is stored on the row as it is written, and the query asks for it:\n *\n * actionGroup the facet's group for the action, answered by the plugin\n * activity registry (`pluginStaffAuditActionGroup`): a\n * registered group by code or by `staffAuditPrefixes`,\n * otherwise the action's leading namespace.\n * kind `access` for an act that only looked (a core read action,\n * or one a plugin declares in `staffAuditAccessActions`),\n * `change` for everything else.\n * targetKind the kind of record acted on: the first segment of the\n * target path (`orgs`, `users`, `hosts`, `lockdowns`).\n * targetHostId the site acted on, when the target is a site or a record\n * under one (`hosts/{id}/…`, `orgs/{org}/hosts/{id}/…`);\n * null otherwise, so \"no site\" is a value a query can ask for.\n * searchTokens word-prefix tokens (`nameSearchTokens`) of the fields a\n * reviewer searches by, for `array-contains` on one word.\n *\n * Every write goes through `withAdminAuditIndex`, on the server through\n * `addAdminAudit` / `setAdminAudit` / `recordAdminAudit` in\n * `@aglyn/tenant-data-admin`. `apps/console/specs/admin-audit-writes-are-stamped.spec.ts`\n * refuses a write to the collection anywhere else, and\n * `apps/console/specs/admin-audit-action-groups.spec.ts` pins the registry's\n * groups and reads to `tools/scripts/lib/admin-audit-index.fixtures.json`,\n * the mapping `tools/scripts/backfill-admin-audit-index.mjs` restamps old\n * rows with. A plugin adding a code, a prefix or a read action therefore\n * fails CI until the fixture names it, and the backfill is re-run.\n */\n\n/** The row's group, as the Action group filter asks for it. */\nexport const ADMIN_AUDIT_GROUP_FIELD = 'actionGroup'\n\n/** Whether the row only looked, as the account page's two tables ask for it. */\nexport const ADMIN_AUDIT_KIND_FIELD = 'kind'\n\n/** The kind of record acted on, as the Target type filter asks for it. */\nexport const ADMIN_AUDIT_TARGET_KIND_FIELD = 'targetKind'\n\n/** The site acted on, as the Site filter asks for it. */\nexport const ADMIN_AUDIT_SITE_FIELD = 'targetHostId'\n\n/** The row's search tokens, as the search asks for one. */\nexport const ADMIN_AUDIT_SEARCH_FIELD = 'searchTokens'\n\n/**\n * The fields a search reaches, in the order they claim the token budget:\n * what was done, who did it, what it was done to, then why.\n */\nexport const ADMIN_AUDIT_SEARCHED_FIELDS = [\n 'action',\n 'actorEmail',\n 'target',\n 'actorUid',\n 'scope',\n 'reason',\n 'note',\n] as const\n\n/**\n * The most tokens one row stores. A free-text note is the only field that\n * can run long, and it is searched last, so it is the one that loses reach\n * past the cap.\n */\nexport const ADMIN_AUDIT_SEARCH_TOKEN_LIMIT = 200\n\n/** Access looked at data; change altered something or acted on someone. */\nexport type AdminAuditKind = 'access' | 'change'\n\n/**\n * The core actions that only LOOKED.\n *\n * An exception list, not a classification of everything, and the default\n * matters more than the membership: anything absent is a `change`. A change\n * is the louder half of the console's audit card, so an action nobody has\n * classified yet gets the MORE prominent treatment rather than the quieter\n * one. The failure mode of the opposite default is an unclassified\n * impersonation rendering as routine browsing.\n *\n * An export is deliberately NOT here. Data leaving the platform is a\n * high-consequence act even though it mutates nothing, and it belongs beside\n * the impersonations rather than beside the record views.\n */\nexport const ADMIN_AUDIT_ACCESS_ACTIONS: readonly string[] = [\n 'email.message-viewed',\n // The acquisition card (AGL-3289). No longer written; the rows already in\n // the log still classify as reads.\n 'user.acquisition-viewed',\n 'org.acquisition-viewed',\n]\n\n/** What a row carries that the stamped fields are derived from. */\nexport type AdminAuditIndexSource = Partial<\n Record<(typeof ADMIN_AUDIT_SEARCHED_FIELDS)[number], unknown>\n>\n\n/** The fields the lists query. */\nexport interface AdminAuditIndexFields {\n actionGroup: string\n kind: AdminAuditKind\n targetKind: string\n targetHostId: string | null\n searchTokens: string[]\n}\n\n/** Separators inside a value: an address's `@` and `.`, a path's `/`, a code's `.`. */\nconst SEPARATORS = /[^\\p{L}\\p{N}]+/gu\n\n/**\n * The search tokens for one row.\n *\n * Each value is tokenized twice: as written, so a typed address or code\n * (`jane@acme`, `org.override`) matches from its start, and split at its\n * separators, so a reader finds `org.override` by `override` and an address\n * by its domain.\n */\nexport function adminAuditSearchTokens(entry: AdminAuditIndexSource): string[] {\n const tokens = new Set<string>()\n for (const field of ADMIN_AUDIT_SEARCHED_FIELDS) {\n const value = entry[field]\n if (typeof value !== 'string' || !value.trim()) continue\n const words = [\n ...nameSearchTokens(value),\n ...nameSearchTokens(value.replace(SEPARATORS, ' ')),\n ]\n for (const token of words) {\n tokens.add(token)\n if (tokens.size >= ADMIN_AUDIT_SEARCH_TOKEN_LIMIT) return [...tokens]\n }\n }\n return [...tokens]\n}\n\n/**\n * The group an action is filed under — the same answer the page's facet\n * offers. Empty for a row with no action.\n */\nexport function adminAuditActionGroup(action: unknown): string {\n return pluginStaffAuditActionGroup(action)\n}\n\n/**\n * An access when the platform or a plugin declares the action a read — a\n * plugin's staff card opening on an org or an account names its own read\n * actions through its activity group (AGL-2939) — and a change otherwise.\n */\nexport function adminAuditKind(action: unknown): AdminAuditKind {\n return typeof action === 'string' &&\n action &&\n (ADMIN_AUDIT_ACCESS_ACTIONS.includes(action) || isPluginStaffAuditAccess(action))\n ? 'access'\n : 'change'\n}\n\n/** The segments of a target path; empty for a target that is not one. */\nconst segmentsOf = (target: unknown): string[] =>\n typeof target === 'string' ? target.trim().split('/').filter(Boolean) : []\n\n/**\n * The kind of record acted on: the target's first segment, which for every\n * path-shaped target is its collection (`orgs/{id}` → `orgs`). A target\n * that is an identifier rather than a path (`sso-domains:acme.com`) is its\n * own type up to the first `:`. Empty for a row with no target.\n */\nexport function adminAuditTargetKind(target: unknown): string {\n const [first = ''] = segmentsOf(target)\n const colon = first.indexOf(':')\n return colon > 0 ? first.slice(0, colon) : first\n}\n\n/**\n * The site acted on: the id after a `hosts` segment in the target path —\n * `hosts/{id}` and anything under it, and a site filed under its\n * organization (`orgs/{org}/hosts/{id}`). Null when the act was not on a site.\n */\nexport function adminAuditTargetHostId(target: unknown): string | null {\n const segments = segmentsOf(target)\n for (let at = 0; at < segments.length - 1; at += 2) {\n if (segments[at] === 'hosts') return segments[at + 1] || null\n }\n return null\n}\n\n/** Every stamped field for one row. */\nexport function adminAuditIndexFields(\n entry: AdminAuditIndexSource,\n): AdminAuditIndexFields {\n return {\n actionGroup: adminAuditActionGroup(entry.action),\n kind: adminAuditKind(entry.action),\n targetKind: adminAuditTargetKind(entry.target),\n targetHostId: adminAuditTargetHostId(entry.target),\n searchTokens: adminAuditSearchTokens(entry),\n }\n}\n\n/**\n * The row as it is stored: the entry, with the fields its lists query.\n * Every write to `adminAudit` passes its data through this.\n */\nexport function withAdminAuditIndex<Entry extends AdminAuditIndexSource>(\n entry: Entry,\n): Entry & AdminAuditIndexFields {\n return { ...entry, ...adminAuditIndexFields(entry) }\n}\n"],"names":["isPluginStaffAuditAccess","pluginStaffAuditActionGroup","nameSearchTokens","ADMIN_AUDIT_GROUP_FIELD","ADMIN_AUDIT_KIND_FIELD","ADMIN_AUDIT_TARGET_KIND_FIELD","ADMIN_AUDIT_SITE_FIELD","ADMIN_AUDIT_SEARCH_FIELD","ADMIN_AUDIT_SEARCHED_FIELDS","ADMIN_AUDIT_SEARCH_TOKEN_LIMIT","ADMIN_AUDIT_ACCESS_ACTIONS","SEPARATORS","adminAuditSearchTokens","entry","tokens","Set","field","value","trim","words","replace","token","add","size","adminAuditActionGroup","action","adminAuditKind","includes","segmentsOf","target","split","filter","Boolean","adminAuditTargetKind","first","colon","indexOf","slice","adminAuditTargetHostId","segments","at","length","adminAuditIndexFields","actionGroup","kind","targetKind","targetHostId","searchTokens","withAdminAuditIndex"],"mappings":"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"}
|
|
@@ -143,7 +143,7 @@ export interface AnalyticsEventParams {
|
|
|
143
143
|
* GA4 recommended. Real account creation only, never a sign-in.
|
|
144
144
|
*
|
|
145
145
|
* The three campaign params are optional and come from
|
|
146
|
-
* `
|
|
146
|
+
* `utmEventParams` (AGL-1731) — present when the signup URL named a
|
|
147
147
|
* campaign, absent entirely otherwise. They are what lets a September ad
|
|
148
148
|
* spend be evaluated: without them a paid click, an organic visit and a
|
|
149
149
|
* partner link arrive indistinguishable and the money cannot be traced to
|
|
@@ -676,8 +676,17 @@ export declare function buildAddToCartParams(input: {
|
|
|
676
676
|
* Callers pass what they already hold — the live-subscribed map in the
|
|
677
677
|
* console, a read snapshot on the server — and never a map read back AFTER
|
|
678
678
|
* the write, which is never empty.
|
|
679
|
+
*
|
|
680
|
+
* ## The placeholder home page is not a publish (AGL-3408)
|
|
681
|
+
*
|
|
682
|
+
* Every new site is created with a home page already routed at `/`, so read
|
|
683
|
+
* literally the map is never empty and no site would ever report a first
|
|
684
|
+
* publish. The host's `defaultHomeScreenId` names that placeholder, and its
|
|
685
|
+
* entry is not counted: the site "came alive" when its owner put something on
|
|
686
|
+
* it, not when the platform did. Republishing the placeholder itself after
|
|
687
|
+
* editing it counts, which is the same act.
|
|
679
688
|
*/
|
|
680
|
-
export declare function isFirstPublishedRoute(routing: Record<string, unknown> | null | undefined): boolean;
|
|
689
|
+
export declare function isFirstPublishedRoute(routing: Record<string, unknown> | null | undefined, defaultHomeScreenId?: string | null): boolean;
|
|
681
690
|
/** The taxonomy, enumerable. */
|
|
682
691
|
export declare const ANALYTICS_EVENT_NAMES: AnalyticsEventName[];
|
|
683
692
|
/**
|
|
@@ -421,8 +421,17 @@ function scrubValue(value) {
|
|
|
421
421
|
* Callers pass what they already hold — the live-subscribed map in the
|
|
422
422
|
* console, a read snapshot on the server — and never a map read back AFTER
|
|
423
423
|
* the write, which is never empty.
|
|
424
|
-
|
|
425
|
-
|
|
424
|
+
*
|
|
425
|
+
* ## The placeholder home page is not a publish (AGL-3408)
|
|
426
|
+
*
|
|
427
|
+
* Every new site is created with a home page already routed at `/`, so read
|
|
428
|
+
* literally the map is never empty and no site would ever report a first
|
|
429
|
+
* publish. The host's `defaultHomeScreenId` names that placeholder, and its
|
|
430
|
+
* entry is not counted: the site "came alive" when its owner put something on
|
|
431
|
+
* it, not when the platform did. Republishing the placeholder itself after
|
|
432
|
+
* editing it counts, which is the same act.
|
|
433
|
+
*/ export function isFirstPublishedRoute(routing, defaultHomeScreenId) {
|
|
434
|
+
return !Object.keys(routing != null ? routing : {}).some((screenId)=>screenId !== defaultHomeScreenId);
|
|
426
435
|
}
|
|
427
436
|
/**
|
|
428
437
|
* The one delivery path, shared by {@link trackEvent} and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/analytics-events.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The one GA4 event taxonomy, shared by the marketing site (tenant runtime)\n * and the console (AGL-1561). See `docs/ANALYTICS.md` for the event map and\n * which GTM-plan §6 metric each event serves.\n *\n * ## Why this module exists\n *\n * Before it, every GA event in the repo was an ad-hoc\n * `;(window as any).gtag?.('event', 'name', {...})` — five of them across the\n * marketing and commerce plugins, plus bare string literals passed to Firebase\n * `logEvent` in the console. Nothing checked the names, nothing checked the\n * params, and a typo produced a silently-missing metric rather than an error.\n * That is the failure mode analytics is worst at surfacing: the number simply\n * reads zero, and zero is indistinguishable from \"nobody did it\".\n *\n * So the names and their params are a TYPE here ({@link AnalyticsEventParams}),\n * and `trackEvent` is generic over it: a misspelled event name or a missing\n * required param is a compile error.\n *\n * That sweep missed five (AGL-1591, closed): the commerce plugin's\n * `view_item` / `add_to_cart` / `begin_checkout` and the marketing runtime's\n * `aglyn_overlay` / `aglyn_experiment`. `window.gtag` is now called in exactly\n * two places in the repo — {@link deliver} below, and `readGaClientId` above,\n * which reads rather than sends.\n *\n * ## Reserved names\n *\n * Where GA4 has a recommended event we use its exact name and its exact param\n * spelling — `sign_up`, `login`, `generate_lead`, `begin_checkout`,\n * `purchase`, `select_content` — so the built-in reports, the funnel\n * explorations and the \"key events\" toggles work without custom definitions.\n * Custom snake_case names appear only where GA4 has no standard: the four\n * activation events, which are Aglyn-specific product milestones.\n *\n * ## Consent (AGL-1498) — the gate is that gtag never loads\n *\n * On tenant sites, including aglyn.com itself, `site-analytics.tsx` renders the\n * gtag `<Script>` pair ONLY when the recorded consent state grants analytics.\n * There is therefore no `window.gtag` at all for a visitor who has not granted,\n * and {@link trackEvent} drops the event on the floor.\n *\n * It drops it — it does not QUEUE it. That distinction is the whole point and\n * `analytics-events.spec.ts` asserts it: an event fired before consent is gone\n * for good, and does not reappear when a later grant loads gtag. A queue would\n * quietly convert \"we did not track you\" into \"we tracked you and waited\", and\n * a replayed hit carries the pre-consent timestamp and page into GA, which is\n * exactly the thing the consent gate exists to prevent. Deliberately no retry,\n * no buffer, no flush-on-grant.\n *\n * ## No PII, enforced rather than promised\n *\n * Every payload passes through {@link sanitizeEventParams} before it reaches a\n * transport: an exact-key denylist drops the identity-bearing params someone\n * will eventually add by reflex (`email`, `org_name`, `first_name`, ...), any\n * value that looks like an email address drops its key entirely, URLs are\n * reduced to origin + pathname so query strings can never smuggle a token or\n * an address, and strings are length-capped. The console separately sets a\n * `user_id` — that is an opaque Firebase uid and is the one identifier GA is\n * allowed to hold.\n *\n * Sanitizing here rather than at each call site is the point: a new call site\n * cannot forget.\n *\n * ## Authored events (AGL-1587)\n *\n * One call site cannot use the taxonomy at all: the `trackGaEvent` action step,\n * whose event name and params are typed by a SITE AUTHOR in the interaction\n * builder. A closed union cannot contain a name nobody has written yet, so\n * {@link trackAuthoredEvent} is the escape hatch — and it is deliberately the\n * only one, so that untrusted input still passes {@link sanitizeEventParams}\n * rather than reaching `window.gtag` raw.\n *\n * The test for which door an event uses is WHO NAMED IT, not whether GA4\n * recommends the name. `aglyn_overlay` and `aglyn_experiment` are outside GA4's\n * recommended set and were candidates for the hatch (AGL-1591); they are in the\n * union instead, because a developer wrote their names and their keys, so they\n * can have compile-time checking — and because the hatch guarantees the\n * opposite of what they need. {@link resolveAuthoredEventName} refuses any name\n * we send — the union AND the server-only names beside it — precisely so that\n * \"not one of ours\" means \"authored\"; put our own events through the hatch and\n * that stops being true, authored hits stop being separable from ours in\n * reports, and an authored `aglyn_experiment` step starts voting in the\n * experiment that decides which variant ships.\n */\n\n/**\n * Read the browser's GA `client_id` — the identifier that ties a hit to a GA\n * user and session.\n *\n * Needed because `purchase` is sent SERVER-side, from the Stripe webhook,\n * where the authoritative money is (see `ga4-measurement-protocol.ts`). The\n * Measurement Protocol requires a `client_id` and a server cannot know one,\n * so it is captured here when checkout starts and carried on the Stripe\n * object's metadata. Without it the revenue still lands, but attached to a\n * synthetic user with no acquisition session — which is exactly the campaign\n * attribution the whole exercise is for.\n *\n * Resolves to null rather than hanging when gtag is absent (no consent, an ad\n * blocker, analytics not configured) or slow to answer. The 500ms cap matters:\n * this sits directly in front of a checkout redirect, and analytics must never\n * be able to delay a payment.\n */\nexport function readGaClientId(\n measurementId: string | undefined | null,\n): Promise<string | null> {\n return new Promise((resolve) => {\n if (typeof window === 'undefined' || !measurementId) return resolve(null)\n const gtag = (window as unknown as { gtag?: unknown }).gtag\n if (typeof gtag !== 'function') return resolve(null)\n let settled = false\n const finish = (value: string | null) => {\n if (settled) return\n settled = true\n resolve(value)\n }\n // Never let a missing callback strand the checkout.\n setTimeout(() => finish(null), 500)\n try {\n ;(gtag as (...args: unknown[]) => void)(\n 'get',\n measurementId,\n 'client_id',\n (id: unknown) => finish(typeof id === 'string' && id ? id : null),\n )\n } catch {\n finish(null)\n }\n })\n}\n\n/** Which door an account was created through (AGL-1497 enumerates all four). */\nexport type SignUpMethod =\n | 'password'\n | 'google_popup'\n | 'google_redirect'\n | 'google_signin'\n\n/** How a returning user authenticated. */\nexport type LoginMethod =\n | 'password'\n | 'google_popup'\n | 'google_redirect'\n | 'sso'\n | 'passkey'\n\n/**\n * A GA4 `items` entry. Only the fields we actually populate — GA accepts more,\n * but an unpopulated field is a column of nulls in every report.\n */\nexport interface AnalyticsItem {\n /** Opaque identifier — a price id, plan key or marketplace listing id. */\n item_id: string\n /** Human-readable product name. NEVER a customer or org name. */\n item_name: string\n /** Distinguishes the revenue lines: `subscription` vs `marketplace`. */\n item_category?: string\n price?: number\n quantity?: number\n}\n\n/**\n * The taxonomy. Adding an event means adding a line here first — which is what\n * makes `docs/ANALYTICS.md` checkable against the code rather than aspirational.\n */\nexport interface AnalyticsEventParams {\n // --- Acquisition (GTM §6: signups, cost/lead by channel) -----------------\n /**\n * GA4 recommended. Real account creation only, never a sign-in.\n *\n * The three campaign params are optional and come from\n * `campaignEventParams` (AGL-1731) — present when the signup URL named a\n * campaign, absent entirely otherwise. They are what lets a September ad\n * spend be evaluated: without them a paid click, an organic visit and a\n * partner link arrive indistinguishable and the money cannot be traced to\n * an account. Named `campaign_*` rather than `utm_*` because these are our\n * own registered dimensions and the `utm_` spellings belong to GA's\n * automatic campaign collection.\n */\n sign_up: {\n method: SignUpMethod\n campaign_source?: string\n campaign_medium?: string\n campaign_name?: string\n }\n /** GA4 recommended. Returning user only. */\n login: { method: LoginMethod }\n /**\n * GA4 recommended. Fired on a SUCCESSFUL form submission — never on click,\n * never on a validation failure. `form_name` is the author-given form name,\n * which is site content and not personal data.\n */\n generate_lead: { form_name: string; form_location?: string }\n /**\n * GA4 recommended. A CTA click, with the section that produced it.\n *\n * Fired by `analytics-link-clicks.ts` (AGL-1562) from a delegated listener,\n * not per call site: an authored page has no code to add a handler to.\n */\n select_content: {\n content_type: string\n content_id: string\n /** Which product surface — see `click.surface`. */\n surface?: string\n }\n\n // --- Activation (GTM §6: % publish a site, % connect Stripe) -------------\n /** Custom: no GA4 equivalent. A new organization exists. */\n org_created: { plan?: string }\n /** Custom: no GA4 equivalent. A new site/host exists. */\n host_created: Record<string, never>\n /**\n * Custom: no GA4 equivalent, and the GTM plan's headline activation metric.\n * A site actually went live.\n */\n site_published: { first_publish?: boolean }\n /** Custom: no GA4 equivalent. Stripe Connect onboarding completed. */\n stripe_connected: Record<string, never>\n\n // --- Revenue (GTM §6: paid conversions, ARPA, annual mix) ---------------\n /** GA4 recommended. A plan checkout started. */\n begin_checkout: {\n currency: string\n value: number\n items: AnalyticsItem[]\n /** `monthly` | `annual` — feeds the §6 annual-mix metric. */\n billing_interval?: string\n }\n /**\n * GA4 recommended. A payment actually succeeded. `transaction_id` is the\n * Stripe object id and is what makes the event idempotent in GA: GA4\n * de-duplicates purchases by transaction id, so a webhook retry cannot\n * inflate revenue.\n */\n purchase: {\n transaction_id: string\n currency: string\n value: number\n items: AnalyticsItem[]\n billing_interval?: string\n /**\n * GA4's shipping charged on the transaction, in currency units. Optional\n * because only a tenant STOREFRONT purchase ships anything — a plan or a\n * marketplace purchase has no shipping to report and omits it.\n *\n * There is deliberately no sibling `tax` (AGL-1639, AGL-1641). The\n * asymmetry is the point and is not an inconsistency to tidy away:\n * `shipping` is a COMPONENT of the `value` beside it, so reporting it\n * describes that value; `tax` is not, because `value` is already ex-tax,\n * so reporting it would assert a relationship that does not hold and\n * invite the subtraction that removes tax a second time.\n */\n shipping?: number\n }\n\n // --- Commerce (tenant storefronts, AGL-1591) -----------------------------\n /**\n * GA4 recommended. A product detail page was viewed on a tenant storefront.\n *\n * Only `items`, and only `item_id`/`item_name` within it: those are the two\n * fields the storefront actually has at this point, and an unpopulated\n * `price`/`item_category` would be a column of nulls in the merchant's\n * reports. The item id is the PRODUCT id — the same id `add_to_cart` and\n * `begin_checkout` use — which is what lets GA join the three into one\n * per-product funnel.\n */\n view_item: { items: AnalyticsItem[] }\n /**\n * GA4 recommended. A storefront product was added to the cart.\n *\n * `currency`/`value` are optional because GA4 pairs them — a `value` with no\n * `currency` is dropped by GA — so they travel together or not at all, and\n * {@link buildAddToCartParams} is what keeps that true. They are populated\n * wherever the surface holds a server-priced figure: the product detail\n * block knows the resolved variant's price and the chosen quantity, so the\n * merchant's \"value added to cart\" is a real number rather than the empty\n * column an items-only hit produces.\n */\n add_to_cart: { currency?: string; value?: number; items: AnalyticsItem[] }\n /**\n * GA4 recommended. The shopper looked at their cart — the funnel step\n * between {@link AnalyticsEventParams.add_to_cart} and\n * {@link AnalyticsEventParams.begin_checkout}, and the one GA4's own\n * shopping-behavior report reads to tell \"never opened the cart\" apart from\n * \"opened it and did not check out\". Without it those two collapse into one\n * drop, and the merchant cannot tell a discovery problem from a pricing one.\n *\n * Fired when the cart is actually ON SCREEN carrying lines — a drawer that\n * opened, or an inline cart block that resolved — never on the badge render\n * that every page of a storefront performs. An empty cart is not a view of\n * anything and reports nothing.\n */\n view_cart: { currency: string; value: number; items: AnalyticsItem[] }\n\n // --- Engagement ---------------------------------------------------------\n /**\n * Custom: no GA4 equivalent. An announcement bar or popup was shown,\n * dismissed or clicked on a tenant site (AGL-200/271).\n *\n * In the taxonomy rather than {@link trackAuthoredEvent} even though it is\n * not a GA4 recommended name: the name and every key here are written by US,\n * which is exactly what the closed union is for. See the note on\n * {@link trackAuthoredEvent} for why the two must not be mixed.\n */\n aglyn_overlay: { overlay_action: string; overlay_id?: string }\n /**\n * Custom: no GA4 equivalent. An experiment exposure or conversion\n * (AGL-253). `experiment_action` is `exposure` | `conversion`.\n *\n * Being in the union also makes the name RESERVED against authored events,\n * which matters more here than anywhere else in this file: these are the\n * counts that decide which variant wins, and a hand-authored\n * `aglyn_experiment` step would silently vote in that election.\n */\n aglyn_experiment: {\n experiment_id: string\n variant_id: string\n experiment_action: string\n }\n /**\n * GA4 recommended-ish. Outbound click to docs, GitHub, etc. Fired by\n * `analytics-link-clicks.ts` (AGL-1562) rather than at a call site.\n */\n click: {\n link_domain: string\n link_id?: string\n /**\n * Which product surface produced the click. GA's built-in Hostname\n * dimension already separates the DOMAINS; this separates surfaces that\n * could share one, and keeps the shared click listener from having to\n * know anything about either.\n *\n * `site` — a tenant published site — is the only value ever SENT. `docs`\n * was designed for (AGL-1579) and is not emitted: `apps/docs` cannot\n * import `libs/`, so this listener is not installed there\n * (docs/ANALYTICS.md decision 7 has the Vercel setting that would change\n * that). Registering `surface` as a dimension and reading a one-value\n * breakdown is the trap — the absent `docs` row means \"no listener\", not\n * \"no clicks\".\n */\n surface?: string\n }\n /**\n * Custom: no GA4 equivalent. One Aglyn Assist message sent (AGL-1860).\n * `tier` is the capability tier served (`free` | `entitled`);\n * `grounded` says whether docs retrieval found sections to cite —\n * ungrounded questions at volume are the docs-gap signal the data loop\n * mines. No question text: params carry no user content.\n */\n assistant_message_sent: { tier: string; grounded: boolean }\n /**\n * Custom: no GA4 equivalent. Explicit thumbs on an Assist answer\n * (AGL-1860). `feedback` is `up` | `down`.\n */\n assistant_feedback: { feedback: string }\n /**\n * Custom: no GA4 equivalent. Aglyn Assist offered to open a page for the\n * user (AGL-1988, level 2). `action` is the registry action id — a closed\n * set, so it carries no user content.\n *\n * The pair below is the only read on whether the confirm gate is a real\n * choice or a speed bump people click through. A shown-to-confirmed ratio\n * near 1 means the card is not being read, and the copy has to change\n * BEFORE the ladder goes anywhere near a write.\n */\n assistant_proposal_shown: { action: string }\n /** Custom: no GA4 equivalent. The user confirmed and was navigated. */\n assistant_proposal_confirmed: { action: string }\n /**\n * Custom: no GA4 equivalent. An AI generation job reached `done`\n * (AGL-2904), observed from the console's jobs drawer. `kind` is the\n * closed `AiJobKind` set and `credits` the job's spend at the plan's\n * credit rate — no brief, no output: params carry no user content.\n */\n ai_job_completed: { kind: string; credits: number }\n /**\n * Custom: no GA4 equivalent. An AI generation job reached `failed`\n * (AGL-2904). `kind` only; the failure text is a fixed customer-safe\n * sentence and says nothing a rate could use.\n */\n ai_job_failed: { kind: string }\n\n // --- Retention (AGL-1859/AGL-1863: the leave path, measurable) -----------\n /**\n * Custom: no GA4 equivalent. The churn survey was answered — step 1 of the\n * cancellation/deletion funnel, and the DENOMINATOR every step below is a\n * rate against. Fired for both leave paths.\n *\n * `reason` is the closed `ChurnSurveyReason` set, never the free-text\n * detail: the detail is customer-written prose, it belongs in Firestore\n * where the data loop reads it, and shipping it to GA would put user\n * content in analytics params. `surface` separates a subscription cancel\n * from an account delete — counting only one understates churn by exactly\n * the orgs that chose the other.\n */\n churn_survey_submitted: { reason: string; surface: string; plan?: string }\n /**\n * Custom: no GA4 equivalent. The customer took the smaller tier instead of\n * leaving — a SAVE, at reduced ARPA. `from_plan`/`to_plan` are what makes\n * that tradeoff measurable rather than a win recorded without its cost.\n */\n downsell_accepted: { from_plan: string; to_plan: string; surface: string }\n /**\n * Custom: no GA4 equivalent. The time-boxed winback discount was accepted.\n *\n * `percent_off` and `duration_months` are reported because the discount is\n * bounded and the bound is the entire point (AGL-1620/AGL-1863): a retained\n * org and the margin it was retained at are one fact, and a save recorded\n * without its price reads as free.\n */\n winback_discount_accepted: {\n percent_off: number\n duration_months: number\n plan?: string\n surface: string\n }\n /**\n * Custom: no GA4 equivalent. They left anyway — the funnel's terminal step,\n * and the numerator for churn.\n *\n * `funnel_completed` is false when the cancel arrived without a funnelId\n * (support ops, Stripe dashboard). It mirrors the `funnelSkipped` marker\n * the routes write, so the GA funnel and the Firestore record agree instead\n * of quietly disagreeing about how many departures were ever surveyed.\n */\n cancellation_completed: {\n surface: string\n plan?: string\n funnel_completed: boolean\n }\n\n // --- Plan changes taken from the grid (AGL-2235, under AGL-1859 §4) ------\n /**\n * Custom: no GA4 equivalent. A downgrade was confirmed and SCHEDULED from\n * the billing plan grid — the same economic event `downsell_accepted`\n * records, arrived at by the other door.\n *\n * Why this has to exist separately: all four retention events above fire\n * from `retention-funnel.dialog.tsx` and from nowhere else. A customer who\n * moves Pro → Starter through the cancel funnel is counted; a customer who\n * moves Pro → Starter by clicking Downgrade on the plan card was counted by\n * nothing. So \"how many orgs moved down\" was unanswerable, and the number\n * that WAS answerable — `downsell_accepted` — undercounted by exactly the\n * share that took the direct route while reading like a total. A save rate\n * computed against it is wrong in the flattering direction, which is the\n * worst direction for a retention number to be wrong in.\n *\n * `effective_at` is the whole point of the event's name: this is the\n * asymmetric-friction arm of AGL-1859 §2, and a downgrade that has been\n * SCHEDULED is not a downgrade that has HAPPENED. Reported as the server's\n * ISO date so the gap between decision and effect — up to a full cycle, and\n * the window in which \"keep my plan\" can still save the org — is visible in\n * the data instead of being collapsed into the decision day.\n *\n * No amount, price or fee: pricing is locked for Sept 1, money belongs to\n * `purchase`/`refund`, and a tier pair already says what changed.\n */\n plan_downgrade_scheduled: {\n from_plan: string\n to_plan: string\n interval: string\n effective_at?: string\n }\n /**\n * Custom: no GA4 equivalent. An existing subscriber moved UP in place.\n *\n * Not `app_upgrade` — that name is GA4-RESERVED (it means an app binary\n * version bump) and a hit using it is dropped, which is silence rather than\n * pollution and therefore the harder failure to notice.\n *\n * `purchase` covers only the Checkout path, so before this, expansion\n * revenue from customers who ALREADY had a subscription was dark: the\n * in-place switch never opens a Checkout and never mints a new\n * subscription, so nothing in the revenue taxonomy saw it. Upgrades are the\n * half of AGL-1859 §2 that is supposed to be frictionless, and an\n * unmeasured half cannot be shown to be.\n */\n plan_upgraded: {\n from_plan: string\n to_plan: string\n interval: string\n }\n}\n\nexport type AnalyticsEventName = keyof AnalyticsEventParams\n\n/**\n * Where a sanitized event goes. The console registers a Firebase\n * `logEvent` transport; the tenant runtime and the plugin bundles have none\n * and fall through to `window.gtag`, which only exists once consent has been\n * granted.\n */\nexport type AnalyticsTransport = (\n name: AnalyticsEventName,\n params: Record<string, unknown>,\n) => void | Promise<void>\n\nlet configuredTransport: AnalyticsTransport | null = null\n\n/**\n * Register the transport for this surface. The console calls this once, with\n * Firebase's `logEvent`, because the console's GA is Firebase-initialised and\n * its `user_id`/user-property state lives on the Firebase Analytics instance —\n * poking `window.gtag` directly there would emit hits that miss it.\n *\n * The tenant runtime deliberately does NOT call this: the plugin bundles run\n * in their own realm and do not share this module instance with the host app,\n * so a module-scope singleton would be invisible to exactly the call sites\n * that need it (the form and newsletter elements). `window.gtag` is the only\n * thing genuinely shared across that boundary, and it is also the consent\n * gate, which makes the fallback the correct primary path there rather than a\n * degraded one.\n */\nexport function configureAnalyticsTransport(\n transport: AnalyticsTransport | null,\n): void {\n configuredTransport = transport\n}\n\n/** Test seam — drops the registered transport. */\nexport function resetAnalyticsTransport(): void {\n configuredTransport = null\n}\n\n/**\n * Param keys that must never reach GA, matched EXACTLY. Substring matching\n * would be wrong in both directions: it would drop the legitimate\n * `form_name` / `item_name` / `link_domain`, and it would still miss a\n * creatively-named new one. The value scan below is the backstop for those.\n */\nconst DENIED_PARAM_KEYS: ReadonlySet<string> = new Set([\n 'email',\n 'email_address',\n 'user_email',\n 'name',\n 'full_name',\n 'first_name',\n 'last_name',\n 'user_name',\n 'username',\n 'customer_name',\n 'org_name',\n 'organization_name',\n 'company',\n 'company_name',\n 'phone',\n 'phone_number',\n 'address',\n 'street',\n 'postal_code',\n 'zip',\n 'ip',\n 'ip_address',\n])\n\n/** Deliberately loose — this is a \"does it smell like an address\" test. */\nconst EMAIL_SHAPED = /[^\\s@]+@[^\\s@]+\\.[^\\s@]+/\n\n/**\n * GA4 truncates param values at 100 chars anyway; do it ourselves, visibly.\n *\n * Exported because an AUTHOR types some of these (the `trackGaEvent` step's\n * parameters), and the field they type into caps its input at the same number\n * — the truncation point and the affordance that describes it have to be one\n * value, or the editor promises a length the delivery quietly shortens.\n */\nexport const ANALYTICS_PARAM_MAX_LENGTH = 100\n\nfunction scrubValue(value: string): string | null {\n let candidate = value\n if (/^https?:\\/\\//i.test(candidate)) {\n try {\n const url = new URL(candidate)\n // Origin + pathname only: a query string is where a session token, a\n // signup email or a Stripe id ends up, and none of them belong in GA.\n candidate = `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n }\n // AFTER the URL reduction, not before. A page URL routinely carries an\n // address in its query (`?email=…` on a prefilled signup link), and testing\n // the raw string would throw the whole URL away for PII that the reduction\n // was about to remove — losing the legitimate page dimension to protect\n // something already protected. Testing the REDUCED value still catches an\n // address embedded in the path itself, which the reduction keeps.\n if (EMAIL_SHAPED.test(candidate)) return null\n return candidate.slice(0, ANALYTICS_PARAM_MAX_LENGTH)\n}\n\n/**\n * Strip anything identity-bearing from an event payload. Exported so\n * `analytics-events.spec.ts` can assert the guarantee directly rather than\n * only through `trackEvent`.\n */\nexport function sanitizeEventParams(\n params: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const safe: Record<string, unknown> = {}\n if (!params) return safe\n for (const [key, value] of Object.entries(params)) {\n if (DENIED_PARAM_KEYS.has(key.toLowerCase())) continue\n if (value === undefined || value === null) continue\n if (typeof value === 'string') {\n const scrubbed = scrubValue(value)\n if (scrubbed === null || scrubbed === '') continue\n safe[key] = scrubbed\n continue\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n safe[key] = value\n continue\n }\n if (Array.isArray(value)) {\n // `items` — sanitize each entry with the same rules.\n safe[key] = value.map((entry) =>\n entry && typeof entry === 'object'\n ? sanitizeEventParams(entry as Record<string, unknown>)\n : entry,\n )\n continue\n }\n if (typeof value === 'object') {\n safe[key] = sanitizeEventParams(value as Record<string, unknown>)\n }\n // Anything else (function, symbol) is dropped.\n }\n return safe\n}\n\n/**\n * Fire a GA4 event.\n *\n * Never throws and never queues. If the surface has no transport and no\n * `window.gtag` — which on a tenant site means the visitor has not granted\n * analytics consent — the event is DROPPED, permanently. See the module\n * comment for why a queue would be the wrong answer.\n */\nexport function trackEvent<K extends AnalyticsEventName>(\n name: K,\n params: AnalyticsEventParams[K],\n): void {\n deliver(name, sanitizeEventParams(params as Record<string, unknown>))\n}\n\n/**\n * How long a navigation may be held waiting for a hit to reach gtag. Short\n * enough to be invisible next to a Stripe redirect, long enough to cover a\n * Firebase Analytics init that has not settled yet.\n */\nconst NAVIGATION_FLUSH_TIMEOUT_MS = 300\n\n/**\n * Fire a GA4 event and resolve once it has been HANDED TO gtag — for the call\n * sites that navigate away in the same tick (AGL-1580).\n *\n * ## What was actually losing the event\n *\n * Not the transport, which was the obvious suspect and the wrong one. Measured\n * against real gtag.js with every transport interposed: gtag flushes its queue\n * on pagehide through `fetch(..., { keepalive: true })`, which is precisely the\n * mechanism that survives a document teardown. Once a hit reaches gtag, a\n * navigation does not destroy it — and `transport_type: 'beacon'`, the standard\n * answer, therefore fixes nothing here.\n *\n * What is lost is the hit that never REACHES gtag. Firebase's `logEvent` is\n * `async` and awaits the SDK's initialization promise before calling gtag\n * (`@firebase/analytics/dist/index.cjs.js`, `logEvent$1`). While that promise is\n * already settled the continuation is a microtask, microtasks drain before the\n * queued navigation task, and the hit gets out. While it is still PENDING — the\n * first checkout of a fresh session, exactly the case that has never yet been\n * seen in the property — the continuation is scheduled behind the navigation\n * and never runs at all. Measured both ways: pending init loses the event,\n * awaiting it delivers it, nothing else changed.\n *\n * ## Why a timeout rather than a bare await\n *\n * A bare `await` on `logEvent` hands the user's redirect to the analytics\n * stack. When the analytics host is blocked — an ad blocker, a corporate proxy,\n * a consent tool that never loads — Firebase's initialization promise can stay\n * pending indefinitely, and the checkout would hang on a metric. So the wait is\n * RACED against {@link NAVIGATION_FLUSH_TIMEOUT_MS}: a blocked analytics stack\n * costs the redirect 300ms once and then it proceeds, which is the same outcome\n * the caller had before this existed. Never rejects, for the same reason\n * {@link trackEvent} never throws.\n *\n * On a surface with no registered transport — the tenant runtime and the plugin\n * bundles, which go straight to `window.gtag` — delivery is synchronous, so\n * this resolves immediately and costs the storefront checkout nothing at all.\n */\nexport async function trackEventBeforeNavigation<K extends AnalyticsEventName>(\n name: K,\n params: AnalyticsEventParams[K],\n): Promise<void> {\n const delivered = deliver(\n name,\n sanitizeEventParams(params as Record<string, unknown>),\n )\n // Synchronous transport (or none): already handed over, nothing to wait for.\n if (!delivered || typeof delivered.then !== 'function') return\n await Promise.race([\n // A transport that REJECTS must not become an unhandled rejection, and\n // must not hold the navigation either — it has already failed.\n Promise.resolve(delivered).catch((): void => undefined),\n new Promise<void>((resolve) =>\n setTimeout(resolve, NAVIGATION_FLUSH_TIMEOUT_MS),\n ),\n ])\n}\n\n/**\n * Build the ONE `begin_checkout` payload, for every surface that starts a\n * checkout (AGL-1591).\n *\n * ## Why a constructor and not just the type\n *\n * Two surfaces fire this name: the console, when a plan checkout starts, and a\n * tenant storefront, when a cart checks out. Until AGL-1591 the storefront\n * fired it raw and carried `value`/`currency` only, so ONE event name arrived\n * in two shapes — and a `begin_checkout` breakdown showed two populations that\n * could not be compared, with the storefront half missing the `items` the GA4\n * ecommerce funnel is built on. Routing both through {@link trackEvent} makes\n * the compiler settle the KEYS.\n *\n * It does not settle the NUMBER, which is the other way two call sites of one\n * event diverge, and the more dangerous one because nothing about it looks\n * wrong: the console's annual plans are priced per-month-billed-yearly, so its\n * `value` is twelve of them, and that only stayed right because a comment said\n * so. Here `value` DERIVES from the items unless a caller states a different\n * one — a cart states its subtotal, which is authoritative after discounts —\n * so \"what the customer is about to be charged\" has one definition rather than\n * one per surface.\n *\n * Server-safe: pure, no DOM, so the Measurement Protocol sender can compose\n * the matching `purchase` items from the same shapes.\n */\nexport function buildBeginCheckoutParams(input: {\n items: AnalyticsItem[]\n /**\n * The amount actually being charged. Defaults to the sum of the items'\n * `price * quantity`, which is right whenever nothing has adjusted it.\n */\n value?: number\n /** ISO 4217. Defaults to `USD`, the only currency either surface bills in. */\n currency?: string\n /** `monthly` | `annual`. Subscriptions only — a storefront cart has none. */\n billingInterval?: string\n}): AnalyticsEventParams['begin_checkout'] {\n return {\n ...priceItems(input),\n ...(input.billingInterval ? { billing_interval: input.billingInterval } : {}),\n }\n}\n\n/**\n * The `currency`/`value`/`items` triple every GA4 ecommerce step shares.\n *\n * Private, because a caller should reach for the named builder for the event\n * it is about to send: the names differ in what they mean by `value` even\n * though the arithmetic is identical, and the JSDoc on each is where that is\n * written down.\n */\nfunction priceItems(input: {\n items: AnalyticsItem[]\n value?: number\n currency?: string\n}): { currency: string; value: number; items: AnalyticsItem[] } {\n const items = input.items ?? []\n const summed = items.reduce(\n (total, item) => total + (item.price ?? 0) * (item.quantity ?? 1),\n 0,\n )\n return {\n currency: input.currency ?? 'USD',\n // Money, so two decimals: a float sum of cents-derived prices produces\n // `59.99999999999999`, and GA would report that verbatim.\n value: Math.round((input.value ?? summed) * 100) / 100,\n items,\n }\n}\n\n/**\n * Build `view_cart` — the shopper is looking at the cart's contents.\n *\n * `value` is the cart's subtotal as the SERVER priced it, which is what makes\n * this comparable with the `begin_checkout` the same cart sends moments later:\n * two steps of one funnel that disagreed about the size of the same cart would\n * read as shoppers editing it between screens.\n */\nexport function buildViewCartParams(input: {\n items: AnalyticsItem[]\n value?: number\n currency?: string\n}): AnalyticsEventParams['view_cart'] {\n return priceItems(input)\n}\n\n/**\n * Build `add_to_cart` — one product going in, not the cart's new total.\n *\n * `value` is what was JUST ADDED (`price * quantity`), which is GA4's\n * definition and the only one that makes the metric additive: summing a\n * running cart total over a session would count the first item once per\n * subsequent add.\n *\n * Callers that cannot price the line — a quick-add with no resolved variant —\n * pass items alone and the pair is omitted rather than reported as zero. A\n * zero here would read as a free product in the merchant's report.\n */\nexport function buildAddToCartParams(input: {\n items: AnalyticsItem[]\n value?: number\n currency?: string\n}): AnalyticsEventParams['add_to_cart'] {\n const priced = priceItems(input)\n if (!(priced.value > 0)) return { items: priced.items }\n return priced\n}\n\n/**\n * Decide `site_published`'s `first_publish` from the host's routing map as it\n * stood BEFORE the route being published was registered (AGL-1588).\n *\n * ## What \"first\" means, and why it is defined once\n *\n * Three call sites report `site_published` — `publishScreenRoute`, the\n * besigner's one-click publish, and the scheduled publish executor, the last\n * of which is server-side and sends over the Measurement Protocol. A\n * breakdown is only worth registering if all three mean the same thing by it,\n * and \"first\" has several plausible readings. This is the one they share:\n *\n * **The host had no live route at all before this one.** Not \"first for the\n * org\" — that needs a cross-host query the server path cannot make, and the\n * scheduled sender's client id is derived from the HOST anyway, so an org is\n * not a thing it can see. Not \"first for this screen\" either, which would be\n * true of every second page a site adds and would make the dimension a\n * synonym for the event.\n *\n * So the metric it separates is the GTM §6 activation one: `first_publish:\n * true` counts sites that came alive, where the event alone counts publishes.\n *\n * ## The one dishonesty, and why it does not matter\n *\n * Unpublishing every route and publishing again reports `true` a second time.\n * Detecting that needs publish history the routing map does not keep. It is\n * harmless for the metric it exists for, because activation is read as the\n * share of USERS who ever sent `first_publish: true`, and a user counted twice\n * is still one user.\n *\n * Callers pass what they already hold — the live-subscribed map in the\n * console, a read snapshot on the server — and never a map read back AFTER\n * the write, which is never empty.\n */\nexport function isFirstPublishedRoute(\n routing: Record<string, unknown> | null | undefined,\n): boolean {\n return Object.keys(routing ?? {}).length === 0\n}\n\n/**\n * The one delivery path, shared by {@link trackEvent} and\n * {@link trackAuthoredEvent}. Takes an ALREADY-sanitized payload — every\n * caller sanitizes first, which is what keeps \"a new call site cannot forget\"\n * true of the authored path too.\n */\nfunction deliver(\n name: string,\n safe: Record<string, unknown>,\n): void | Promise<void> {\n try {\n if (configuredTransport) {\n // The transport's name parameter is the taxonomy union, which an\n // authored name is by definition outside. Nominal only: the console is\n // the sole surface that registers one and it has no authored events\n // (the interaction runtime is tenant-side), and Firebase `logEvent`\n // takes an arbitrary string regardless.\n // Returned, not discarded (AGL-1580). A transport may be ASYNC —\n // Firebase's `logEvent` awaits the SDK's initialization promise before it\n // reaches gtag at all — and a caller that is about to navigate has to be\n // able to wait for it. See `trackEventBeforeNavigation`.\n return configuredTransport(name as AnalyticsEventName, safe)\n }\n if (typeof window === 'undefined') return\n const gtag = (window as unknown as { gtag?: unknown }).gtag\n if (typeof gtag !== 'function') return\n // Synchronous, so nothing is returned and nothing needs awaiting: by the\n // time this call has returned, gtag.js already holds the hit.\n ;(gtag as (...args: unknown[]) => void)('event', name, safe)\n } catch {\n // Analytics never breaks the page — the same posture as the error beacon\n // and the pageview beacon.\n }\n}\n\n/**\n * The taxonomy's names at RUN time. A `Record<AnalyticsEventName, true>` rather\n * than a hand-kept array so the compiler enforces both directions: adding an\n * event to {@link AnalyticsEventParams} without adding it here is a missing-key\n * error, and a name here that is not in the taxonomy is an excess-property one.\n *\n * It exists for {@link trackAuthoredEvent}, which has to refuse these names —\n * a drifting copy would silently re-open the collision it is here to close.\n */\nconst TAXONOMY_EVENT_NAMES: Record<AnalyticsEventName, true> = {\n sign_up: true,\n login: true,\n generate_lead: true,\n select_content: true,\n org_created: true,\n host_created: true,\n site_published: true,\n stripe_connected: true,\n begin_checkout: true,\n purchase: true,\n view_item: true,\n add_to_cart: true,\n view_cart: true,\n aglyn_overlay: true,\n aglyn_experiment: true,\n click: true,\n assistant_message_sent: true,\n assistant_feedback: true,\n assistant_proposal_shown: true,\n assistant_proposal_confirmed: true,\n ai_job_completed: true,\n ai_job_failed: true,\n churn_survey_submitted: true,\n downsell_accepted: true,\n winback_discount_accepted: true,\n cancellation_completed: true,\n plan_downgrade_scheduled: true,\n plan_upgraded: true,\n}\n\n/** The taxonomy, enumerable. */\nexport const ANALYTICS_EVENT_NAMES = Object.keys(\n TAXONOMY_EVENT_NAMES,\n) as AnalyticsEventName[]\n\n/**\n * Event names WE send that this module's union cannot hold, because nothing\n * client-side ever fires them: they are emitted only by the Measurement\n * Protocol sender (`ga4-measurement-protocol.ts`), from a Stripe webhook.\n *\n * They still have to be RESERVED against authored names, and the reason is\n * the same one that puts `purchase` in the union — only less obvious, which\n * is why it was missed. `aglyn.com` is itself a tenant site, pointed at the\n * platform measurement id (`site-analytics.tsx`), so an authored\n * `trackGaEvent` step on our own marketing site lands in the SAME property as\n * these server hits. An authored `refund` does not merely add noise: GA4\n * treats `refund` as ecommerce and SUBTRACTS its `value` from reported\n * revenue, so a mistyped step could walk real money out of the report — the\n * `purchase` hazard, running in the direction nobody audits.\n *\n * Kept as a separate list rather than folded into {@link AnalyticsEventParams}\n * on purpose: adding them to the union would give {@link trackEvent} a\n * client-side door to events that must only ever come from the server, where\n * the authoritative money is. A name here is ours, is never sent from a\n * browser, and is never available to an author.\n *\n * This list is the reason \"not in {@link ANALYTICS_EVENT_NAMES}\" is NOT on its\n * own a sound test for \"authored\" — use {@link isReservedAnalyticsEventName}.\n */\nconst SERVER_ONLY_EVENT_NAMES: ReadonlySet<string> = new Set([\n 'refund',\n 'subscription_cancelled',\n])\n\n/**\n * Every event name Aglyn itself sends, from any surface — the union plus the\n * server-only names above. Exported so a caller can ask the question the two\n * separate lists no longer answer alone.\n */\nexport function isReservedAnalyticsEventName(name: string): boolean {\n return (\n TAXONOMY_EVENT_NAMES[name as AnalyticsEventName] === true ||\n SERVER_ONLY_EVENT_NAMES.has(name)\n )\n}\n\n/**\n * GA4's own reserved event names — GA drops a hit that uses one, so sending it\n * is not pollution but silence, which is the worse failure of the two because\n * nothing anywhere says so.\n */\nconst GA4_RESERVED_EVENT_NAMES: ReadonlySet<string> = new Set([\n 'ad_activeview',\n 'ad_click',\n 'ad_exposure',\n 'ad_impression',\n 'ad_query',\n 'ad_reward',\n 'adunit_exposure',\n 'app_background',\n 'app_clear_data',\n 'app_exception',\n 'app_install',\n 'app_remove',\n 'app_store_refund',\n 'app_store_subscription_cancel',\n 'app_store_subscription_convert',\n 'app_store_subscription_renew',\n 'app_update',\n 'app_upgrade',\n 'dynamic_link_app_open',\n 'dynamic_link_app_update',\n 'dynamic_link_first_open',\n 'error',\n 'first_open',\n 'first_visit',\n 'in_app_purchase',\n 'notification_dismiss',\n 'notification_foreground',\n 'notification_open',\n 'notification_receive',\n 'os_update',\n 'screen_view',\n 'session_start',\n 'user_engagement',\n])\n\n/** GA4 reserves these prefixes outright, whatever follows them. */\nconst GA4_RESERVED_PREFIXES = ['firebase_', 'google_', 'ga_'] as const\n\n/** GA4's hard limit on an event name. Over it, GA drops the event. */\nconst MAX_EVENT_NAME_LENGTH = 40\n\n/**\n * The outcome of putting an authored name through GA4's rules, so the\n * interaction builder can say WHY it refused a name and the runtime can drop\n * the event for the same reason.\n */\nexport interface ResolvedAuthoredEventName {\n /** The name to send, or null when the event must not be sent at all. */\n name: string | null\n /**\n * `reserved` — collides with the taxonomy or with GA4's own names.\n * `unusable` — nothing survives normalization (empty, or no leading letter).\n */\n reason?: 'reserved' | 'unusable'\n}\n\n/**\n * Put an authored event name through GA4's naming rules and our own.\n *\n * Normalization is forgiving on purpose: `\"CTA Click!\"` becomes `cta_click`\n * and still reports, where GA would have dropped it. Names already sitting in\n * published sites were never validated, so refusing them outright would delete\n * working metrics from a paying customer's property to fix a formatting nit.\n *\n * Collisions, in contrast, are refused rather than rewritten. On a tenant site\n * the authored events and OUR events (`generate_lead` from the form element,\n * `select_content`/`click` from the link listener) land in the same property,\n * so an authored `purchase` does not merely add noise — it mixes hand-authored\n * hits into a real revenue number. Refusing is also what keeps authored events\n * separable in reports: an event that is not one of {@link ANALYTICS_EVENT_NAMES}\n * is, by construction, authored.\n *\n * Deliberately NOT prefixed (`site_*`) to achieve that separation. A prefix\n * would rename events already flowing into customers' properties and break\n * every report and key-event conversion configured on the old name — a\n * migration cost paid by people who did nothing wrong.\n */\nexport function resolveAuthoredEventName(\n raw: string | undefined | null,\n): ResolvedAuthoredEventName {\n const normalized = String(raw ?? '')\n .trim()\n .toLowerCase()\n // Anything GA4 does not allow in a name becomes an underscore...\n .replace(/[^a-z0-9_]+/g, '_')\n // ...and a name must START with a letter, so drop what precedes one.\n .replace(/^[^a-z]+/, '')\n .replace(/_{2,}/g, '_')\n .slice(0, MAX_EVENT_NAME_LENGTH)\n // Truncation can leave a trailing underscore; so can the substitution.\n .replace(/_+$/, '')\n if (!normalized) return { name: null, reason: 'unusable' }\n if (\n isReservedAnalyticsEventName(normalized) ||\n GA4_RESERVED_EVENT_NAMES.has(normalized) ||\n GA4_RESERVED_PREFIXES.some((prefix) => normalized.startsWith(prefix))\n ) {\n return { name: null, reason: 'reserved' }\n }\n return { name: normalized }\n}\n\n/** One warning per distinct name per page load — an `everyTime` automation\n * would otherwise fill the console with the same line. */\nconst warnedAuthoredNames = new Set<string>()\n\n/**\n * Fire an event whose name and params were written by a SITE AUTHOR, not by a\n * developer — today only the `trackGaEvent` action step (AGL-1587).\n *\n * Same consent gate, same sanitizer, same drop-never-queue posture as\n * {@link trackEvent}; the only difference is that the name is checked at run\n * time instead of by the compiler, because there is no compiler between the\n * interaction builder and here.\n *\n * A refused event is dropped and warned about in the console rather than\n * surfaced in the page. Nothing here can reach the author — the code is\n * running for a VISITOR of their site, and turning the author's configuration\n * mistake into something a visitor sees would be a worse bug than the missing\n * metric. The author-facing half lives in `validateHostAction`, which refuses\n * to save a name this function would refuse to send, so a silent drop should\n * only ever happen to a step authored before AGL-1587.\n */\nexport function trackAuthoredEvent(\n name: string | undefined | null,\n params?: Record<string, unknown> | null,\n): void {\n const resolved = resolveAuthoredEventName(name)\n if (!resolved.name) {\n const key = String(name ?? '')\n if (!warnedAuthoredNames.has(key)) {\n warnedAuthoredNames.add(key)\n try {\n console.warn(\n `[aglyn] analytics: the event \"${key}\" was not sent — ` +\n (resolved.reason === 'reserved'\n ? 'that name is reserved. Rename the step in the interaction builder.'\n : 'an event name must start with a letter.'),\n )\n } catch {\n // A console that throws is still not worth breaking the page for.\n }\n }\n return\n }\n deliver(resolved.name, sanitizeEventParams(params ?? undefined))\n}\n\n/** Test seam — forgets which authored names have already been warned about. */\nexport function resetAuthoredEventWarnings(): void {\n warnedAuthoredNames.clear()\n}\n"],"names":["readGaClientId","measurementId","Promise","resolve","window","gtag","settled","finish","value","setTimeout","id","configuredTransport","configureAnalyticsTransport","transport","resetAnalyticsTransport","DENIED_PARAM_KEYS","Set","EMAIL_SHAPED","ANALYTICS_PARAM_MAX_LENGTH","scrubValue","candidate","test","url","URL","origin","pathname","slice","sanitizeEventParams","params","safe","key","Object","entries","has","toLowerCase","undefined","scrubbed","Array","isArray","map","entry","trackEvent","name","deliver","NAVIGATION_FLUSH_TIMEOUT_MS","trackEventBeforeNavigation","delivered","then","race","catch","buildBeginCheckoutParams","input","priceItems","billingInterval","billing_interval","items","summed","reduce","total","item","price","quantity","currency","Math","round","buildViewCartParams","buildAddToCartParams","priced","isFirstPublishedRoute","routing","keys","length","TAXONOMY_EVENT_NAMES","sign_up","login","generate_lead","select_content","org_created","host_created","site_published","stripe_connected","begin_checkout","purchase","view_item","add_to_cart","view_cart","aglyn_overlay","aglyn_experiment","click","assistant_message_sent","assistant_feedback","assistant_proposal_shown","assistant_proposal_confirmed","ai_job_completed","ai_job_failed","churn_survey_submitted","downsell_accepted","winback_discount_accepted","cancellation_completed","plan_downgrade_scheduled","plan_upgraded","ANALYTICS_EVENT_NAMES","SERVER_ONLY_EVENT_NAMES","isReservedAnalyticsEventName","GA4_RESERVED_EVENT_NAMES","GA4_RESERVED_PREFIXES","MAX_EVENT_NAME_LENGTH","resolveAuthoredEventName","raw","normalized","String","trim","replace","reason","some","prefix","startsWith","warnedAuthoredNames","trackAuthoredEvent","resolved","add","console","warn","resetAuthoredEventWarnings","clear"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/analytics-events.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The one GA4 event taxonomy, shared by the marketing site (tenant runtime)\n * and the console (AGL-1561). See `docs/ANALYTICS.md` for the event map and\n * which GTM-plan §6 metric each event serves.\n *\n * ## Why this module exists\n *\n * Before it, every GA event in the repo was an ad-hoc\n * `;(window as any).gtag?.('event', 'name', {...})` — five of them across the\n * marketing and commerce plugins, plus bare string literals passed to Firebase\n * `logEvent` in the console. Nothing checked the names, nothing checked the\n * params, and a typo produced a silently-missing metric rather than an error.\n * That is the failure mode analytics is worst at surfacing: the number simply\n * reads zero, and zero is indistinguishable from \"nobody did it\".\n *\n * So the names and their params are a TYPE here ({@link AnalyticsEventParams}),\n * and `trackEvent` is generic over it: a misspelled event name or a missing\n * required param is a compile error.\n *\n * That sweep missed five (AGL-1591, closed): the commerce plugin's\n * `view_item` / `add_to_cart` / `begin_checkout` and the marketing runtime's\n * `aglyn_overlay` / `aglyn_experiment`. `window.gtag` is now called in exactly\n * two places in the repo — {@link deliver} below, and `readGaClientId` above,\n * which reads rather than sends.\n *\n * ## Reserved names\n *\n * Where GA4 has a recommended event we use its exact name and its exact param\n * spelling — `sign_up`, `login`, `generate_lead`, `begin_checkout`,\n * `purchase`, `select_content` — so the built-in reports, the funnel\n * explorations and the \"key events\" toggles work without custom definitions.\n * Custom snake_case names appear only where GA4 has no standard: the four\n * activation events, which are Aglyn-specific product milestones.\n *\n * ## Consent (AGL-1498) — the gate is that gtag never loads\n *\n * On tenant sites, including aglyn.com itself, `site-analytics.tsx` renders the\n * gtag `<Script>` pair ONLY when the recorded consent state grants analytics.\n * There is therefore no `window.gtag` at all for a visitor who has not granted,\n * and {@link trackEvent} drops the event on the floor.\n *\n * It drops it — it does not QUEUE it. That distinction is the whole point and\n * `analytics-events.spec.ts` asserts it: an event fired before consent is gone\n * for good, and does not reappear when a later grant loads gtag. A queue would\n * quietly convert \"we did not track you\" into \"we tracked you and waited\", and\n * a replayed hit carries the pre-consent timestamp and page into GA, which is\n * exactly the thing the consent gate exists to prevent. Deliberately no retry,\n * no buffer, no flush-on-grant.\n *\n * ## No PII, enforced rather than promised\n *\n * Every payload passes through {@link sanitizeEventParams} before it reaches a\n * transport: an exact-key denylist drops the identity-bearing params someone\n * will eventually add by reflex (`email`, `org_name`, `first_name`, ...), any\n * value that looks like an email address drops its key entirely, URLs are\n * reduced to origin + pathname so query strings can never smuggle a token or\n * an address, and strings are length-capped. The console separately sets a\n * `user_id` — that is an opaque Firebase uid and is the one identifier GA is\n * allowed to hold.\n *\n * Sanitizing here rather than at each call site is the point: a new call site\n * cannot forget.\n *\n * ## Authored events (AGL-1587)\n *\n * One call site cannot use the taxonomy at all: the `trackGaEvent` action step,\n * whose event name and params are typed by a SITE AUTHOR in the interaction\n * builder. A closed union cannot contain a name nobody has written yet, so\n * {@link trackAuthoredEvent} is the escape hatch — and it is deliberately the\n * only one, so that untrusted input still passes {@link sanitizeEventParams}\n * rather than reaching `window.gtag` raw.\n *\n * The test for which door an event uses is WHO NAMED IT, not whether GA4\n * recommends the name. `aglyn_overlay` and `aglyn_experiment` are outside GA4's\n * recommended set and were candidates for the hatch (AGL-1591); they are in the\n * union instead, because a developer wrote their names and their keys, so they\n * can have compile-time checking — and because the hatch guarantees the\n * opposite of what they need. {@link resolveAuthoredEventName} refuses any name\n * we send — the union AND the server-only names beside it — precisely so that\n * \"not one of ours\" means \"authored\"; put our own events through the hatch and\n * that stops being true, authored hits stop being separable from ours in\n * reports, and an authored `aglyn_experiment` step starts voting in the\n * experiment that decides which variant ships.\n */\n\n/**\n * Read the browser's GA `client_id` — the identifier that ties a hit to a GA\n * user and session.\n *\n * Needed because `purchase` is sent SERVER-side, from the Stripe webhook,\n * where the authoritative money is (see `ga4-measurement-protocol.ts`). The\n * Measurement Protocol requires a `client_id` and a server cannot know one,\n * so it is captured here when checkout starts and carried on the Stripe\n * object's metadata. Without it the revenue still lands, but attached to a\n * synthetic user with no acquisition session — which is exactly the campaign\n * attribution the whole exercise is for.\n *\n * Resolves to null rather than hanging when gtag is absent (no consent, an ad\n * blocker, analytics not configured) or slow to answer. The 500ms cap matters:\n * this sits directly in front of a checkout redirect, and analytics must never\n * be able to delay a payment.\n */\nexport function readGaClientId(\n measurementId: string | undefined | null,\n): Promise<string | null> {\n return new Promise((resolve) => {\n if (typeof window === 'undefined' || !measurementId) return resolve(null)\n const gtag = (window as unknown as { gtag?: unknown }).gtag\n if (typeof gtag !== 'function') return resolve(null)\n let settled = false\n const finish = (value: string | null) => {\n if (settled) return\n settled = true\n resolve(value)\n }\n // Never let a missing callback strand the checkout.\n setTimeout(() => finish(null), 500)\n try {\n ;(gtag as (...args: unknown[]) => void)(\n 'get',\n measurementId,\n 'client_id',\n (id: unknown) => finish(typeof id === 'string' && id ? id : null),\n )\n } catch {\n finish(null)\n }\n })\n}\n\n/** Which door an account was created through (AGL-1497 enumerates all four). */\nexport type SignUpMethod =\n | 'password'\n | 'google_popup'\n | 'google_redirect'\n | 'google_signin'\n\n/** How a returning user authenticated. */\nexport type LoginMethod =\n | 'password'\n | 'google_popup'\n | 'google_redirect'\n | 'sso'\n | 'passkey'\n\n/**\n * A GA4 `items` entry. Only the fields we actually populate — GA accepts more,\n * but an unpopulated field is a column of nulls in every report.\n */\nexport interface AnalyticsItem {\n /** Opaque identifier — a price id, plan key or marketplace listing id. */\n item_id: string\n /** Human-readable product name. NEVER a customer or org name. */\n item_name: string\n /** Distinguishes the revenue lines: `subscription` vs `marketplace`. */\n item_category?: string\n price?: number\n quantity?: number\n}\n\n/**\n * The taxonomy. Adding an event means adding a line here first — which is what\n * makes `docs/ANALYTICS.md` checkable against the code rather than aspirational.\n */\nexport interface AnalyticsEventParams {\n // --- Acquisition (GTM §6: signups, cost/lead by channel) -----------------\n /**\n * GA4 recommended. Real account creation only, never a sign-in.\n *\n * The three campaign params are optional and come from\n * `utmEventParams` (AGL-1731) — present when the signup URL named a\n * campaign, absent entirely otherwise. They are what lets a September ad\n * spend be evaluated: without them a paid click, an organic visit and a\n * partner link arrive indistinguishable and the money cannot be traced to\n * an account. Named `campaign_*` rather than `utm_*` because these are our\n * own registered dimensions and the `utm_` spellings belong to GA's\n * automatic campaign collection.\n */\n sign_up: {\n method: SignUpMethod\n campaign_source?: string\n campaign_medium?: string\n campaign_name?: string\n }\n /** GA4 recommended. Returning user only. */\n login: { method: LoginMethod }\n /**\n * GA4 recommended. Fired on a SUCCESSFUL form submission — never on click,\n * never on a validation failure. `form_name` is the author-given form name,\n * which is site content and not personal data.\n */\n generate_lead: { form_name: string; form_location?: string }\n /**\n * GA4 recommended. A CTA click, with the section that produced it.\n *\n * Fired by `analytics-link-clicks.ts` (AGL-1562) from a delegated listener,\n * not per call site: an authored page has no code to add a handler to.\n */\n select_content: {\n content_type: string\n content_id: string\n /** Which product surface — see `click.surface`. */\n surface?: string\n }\n\n // --- Activation (GTM §6: % publish a site, % connect Stripe) -------------\n /** Custom: no GA4 equivalent. A new organization exists. */\n org_created: { plan?: string }\n /** Custom: no GA4 equivalent. A new site/host exists. */\n host_created: Record<string, never>\n /**\n * Custom: no GA4 equivalent, and the GTM plan's headline activation metric.\n * A site actually went live.\n */\n site_published: { first_publish?: boolean }\n /** Custom: no GA4 equivalent. Stripe Connect onboarding completed. */\n stripe_connected: Record<string, never>\n\n // --- Revenue (GTM §6: paid conversions, ARPA, annual mix) ---------------\n /** GA4 recommended. A plan checkout started. */\n begin_checkout: {\n currency: string\n value: number\n items: AnalyticsItem[]\n /** `monthly` | `annual` — feeds the §6 annual-mix metric. */\n billing_interval?: string\n }\n /**\n * GA4 recommended. A payment actually succeeded. `transaction_id` is the\n * Stripe object id and is what makes the event idempotent in GA: GA4\n * de-duplicates purchases by transaction id, so a webhook retry cannot\n * inflate revenue.\n */\n purchase: {\n transaction_id: string\n currency: string\n value: number\n items: AnalyticsItem[]\n billing_interval?: string\n /**\n * GA4's shipping charged on the transaction, in currency units. Optional\n * because only a tenant STOREFRONT purchase ships anything — a plan or a\n * marketplace purchase has no shipping to report and omits it.\n *\n * There is deliberately no sibling `tax` (AGL-1639, AGL-1641). The\n * asymmetry is the point and is not an inconsistency to tidy away:\n * `shipping` is a COMPONENT of the `value` beside it, so reporting it\n * describes that value; `tax` is not, because `value` is already ex-tax,\n * so reporting it would assert a relationship that does not hold and\n * invite the subtraction that removes tax a second time.\n */\n shipping?: number\n }\n\n // --- Commerce (tenant storefronts, AGL-1591) -----------------------------\n /**\n * GA4 recommended. A product detail page was viewed on a tenant storefront.\n *\n * Only `items`, and only `item_id`/`item_name` within it: those are the two\n * fields the storefront actually has at this point, and an unpopulated\n * `price`/`item_category` would be a column of nulls in the merchant's\n * reports. The item id is the PRODUCT id — the same id `add_to_cart` and\n * `begin_checkout` use — which is what lets GA join the three into one\n * per-product funnel.\n */\n view_item: { items: AnalyticsItem[] }\n /**\n * GA4 recommended. A storefront product was added to the cart.\n *\n * `currency`/`value` are optional because GA4 pairs them — a `value` with no\n * `currency` is dropped by GA — so they travel together or not at all, and\n * {@link buildAddToCartParams} is what keeps that true. They are populated\n * wherever the surface holds a server-priced figure: the product detail\n * block knows the resolved variant's price and the chosen quantity, so the\n * merchant's \"value added to cart\" is a real number rather than the empty\n * column an items-only hit produces.\n */\n add_to_cart: { currency?: string; value?: number; items: AnalyticsItem[] }\n /**\n * GA4 recommended. The shopper looked at their cart — the funnel step\n * between {@link AnalyticsEventParams.add_to_cart} and\n * {@link AnalyticsEventParams.begin_checkout}, and the one GA4's own\n * shopping-behavior report reads to tell \"never opened the cart\" apart from\n * \"opened it and did not check out\". Without it those two collapse into one\n * drop, and the merchant cannot tell a discovery problem from a pricing one.\n *\n * Fired when the cart is actually ON SCREEN carrying lines — a drawer that\n * opened, or an inline cart block that resolved — never on the badge render\n * that every page of a storefront performs. An empty cart is not a view of\n * anything and reports nothing.\n */\n view_cart: { currency: string; value: number; items: AnalyticsItem[] }\n\n // --- Engagement ---------------------------------------------------------\n /**\n * Custom: no GA4 equivalent. An announcement bar or popup was shown,\n * dismissed or clicked on a tenant site (AGL-200/271).\n *\n * In the taxonomy rather than {@link trackAuthoredEvent} even though it is\n * not a GA4 recommended name: the name and every key here are written by US,\n * which is exactly what the closed union is for. See the note on\n * {@link trackAuthoredEvent} for why the two must not be mixed.\n */\n aglyn_overlay: { overlay_action: string; overlay_id?: string }\n /**\n * Custom: no GA4 equivalent. An experiment exposure or conversion\n * (AGL-253). `experiment_action` is `exposure` | `conversion`.\n *\n * Being in the union also makes the name RESERVED against authored events,\n * which matters more here than anywhere else in this file: these are the\n * counts that decide which variant wins, and a hand-authored\n * `aglyn_experiment` step would silently vote in that election.\n */\n aglyn_experiment: {\n experiment_id: string\n variant_id: string\n experiment_action: string\n }\n /**\n * GA4 recommended-ish. Outbound click to docs, GitHub, etc. Fired by\n * `analytics-link-clicks.ts` (AGL-1562) rather than at a call site.\n */\n click: {\n link_domain: string\n link_id?: string\n /**\n * Which product surface produced the click. GA's built-in Hostname\n * dimension already separates the DOMAINS; this separates surfaces that\n * could share one, and keeps the shared click listener from having to\n * know anything about either.\n *\n * `site` — a tenant published site — is the only value ever SENT. `docs`\n * was designed for (AGL-1579) and is not emitted: `apps/docs` cannot\n * import `libs/`, so this listener is not installed there\n * (docs/ANALYTICS.md decision 7 has the Vercel setting that would change\n * that). Registering `surface` as a dimension and reading a one-value\n * breakdown is the trap — the absent `docs` row means \"no listener\", not\n * \"no clicks\".\n */\n surface?: string\n }\n /**\n * Custom: no GA4 equivalent. One Aglyn Assist message sent (AGL-1860).\n * `tier` is the capability tier served (`free` | `entitled`);\n * `grounded` says whether docs retrieval found sections to cite —\n * ungrounded questions at volume are the docs-gap signal the data loop\n * mines. No question text: params carry no user content.\n */\n assistant_message_sent: { tier: string; grounded: boolean }\n /**\n * Custom: no GA4 equivalent. Explicit thumbs on an Assist answer\n * (AGL-1860). `feedback` is `up` | `down`.\n */\n assistant_feedback: { feedback: string }\n /**\n * Custom: no GA4 equivalent. Aglyn Assist offered to open a page for the\n * user (AGL-1988, level 2). `action` is the registry action id — a closed\n * set, so it carries no user content.\n *\n * The pair below is the only read on whether the confirm gate is a real\n * choice or a speed bump people click through. A shown-to-confirmed ratio\n * near 1 means the card is not being read, and the copy has to change\n * BEFORE the ladder goes anywhere near a write.\n */\n assistant_proposal_shown: { action: string }\n /** Custom: no GA4 equivalent. The user confirmed and was navigated. */\n assistant_proposal_confirmed: { action: string }\n /**\n * Custom: no GA4 equivalent. An AI generation job reached `done`\n * (AGL-2904), observed from the console's jobs drawer. `kind` is the\n * closed `AiJobKind` set and `credits` the job's spend at the plan's\n * credit rate — no brief, no output: params carry no user content.\n */\n ai_job_completed: { kind: string; credits: number }\n /**\n * Custom: no GA4 equivalent. An AI generation job reached `failed`\n * (AGL-2904). `kind` only; the failure text is a fixed customer-safe\n * sentence and says nothing a rate could use.\n */\n ai_job_failed: { kind: string }\n\n // --- Retention (AGL-1859/AGL-1863: the leave path, measurable) -----------\n /**\n * Custom: no GA4 equivalent. The churn survey was answered — step 1 of the\n * cancellation/deletion funnel, and the DENOMINATOR every step below is a\n * rate against. Fired for both leave paths.\n *\n * `reason` is the closed `ChurnSurveyReason` set, never the free-text\n * detail: the detail is customer-written prose, it belongs in Firestore\n * where the data loop reads it, and shipping it to GA would put user\n * content in analytics params. `surface` separates a subscription cancel\n * from an account delete — counting only one understates churn by exactly\n * the orgs that chose the other.\n */\n churn_survey_submitted: { reason: string; surface: string; plan?: string }\n /**\n * Custom: no GA4 equivalent. The customer took the smaller tier instead of\n * leaving — a SAVE, at reduced ARPA. `from_plan`/`to_plan` are what makes\n * that tradeoff measurable rather than a win recorded without its cost.\n */\n downsell_accepted: { from_plan: string; to_plan: string; surface: string }\n /**\n * Custom: no GA4 equivalent. The time-boxed winback discount was accepted.\n *\n * `percent_off` and `duration_months` are reported because the discount is\n * bounded and the bound is the entire point (AGL-1620/AGL-1863): a retained\n * org and the margin it was retained at are one fact, and a save recorded\n * without its price reads as free.\n */\n winback_discount_accepted: {\n percent_off: number\n duration_months: number\n plan?: string\n surface: string\n }\n /**\n * Custom: no GA4 equivalent. They left anyway — the funnel's terminal step,\n * and the numerator for churn.\n *\n * `funnel_completed` is false when the cancel arrived without a funnelId\n * (support ops, Stripe dashboard). It mirrors the `funnelSkipped` marker\n * the routes write, so the GA funnel and the Firestore record agree instead\n * of quietly disagreeing about how many departures were ever surveyed.\n */\n cancellation_completed: {\n surface: string\n plan?: string\n funnel_completed: boolean\n }\n\n // --- Plan changes taken from the grid (AGL-2235, under AGL-1859 §4) ------\n /**\n * Custom: no GA4 equivalent. A downgrade was confirmed and SCHEDULED from\n * the billing plan grid — the same economic event `downsell_accepted`\n * records, arrived at by the other door.\n *\n * Why this has to exist separately: all four retention events above fire\n * from `retention-funnel.dialog.tsx` and from nowhere else. A customer who\n * moves Pro → Starter through the cancel funnel is counted; a customer who\n * moves Pro → Starter by clicking Downgrade on the plan card was counted by\n * nothing. So \"how many orgs moved down\" was unanswerable, and the number\n * that WAS answerable — `downsell_accepted` — undercounted by exactly the\n * share that took the direct route while reading like a total. A save rate\n * computed against it is wrong in the flattering direction, which is the\n * worst direction for a retention number to be wrong in.\n *\n * `effective_at` is the whole point of the event's name: this is the\n * asymmetric-friction arm of AGL-1859 §2, and a downgrade that has been\n * SCHEDULED is not a downgrade that has HAPPENED. Reported as the server's\n * ISO date so the gap between decision and effect — up to a full cycle, and\n * the window in which \"keep my plan\" can still save the org — is visible in\n * the data instead of being collapsed into the decision day.\n *\n * No amount, price or fee: pricing is locked for Sept 1, money belongs to\n * `purchase`/`refund`, and a tier pair already says what changed.\n */\n plan_downgrade_scheduled: {\n from_plan: string\n to_plan: string\n interval: string\n effective_at?: string\n }\n /**\n * Custom: no GA4 equivalent. An existing subscriber moved UP in place.\n *\n * Not `app_upgrade` — that name is GA4-RESERVED (it means an app binary\n * version bump) and a hit using it is dropped, which is silence rather than\n * pollution and therefore the harder failure to notice.\n *\n * `purchase` covers only the Checkout path, so before this, expansion\n * revenue from customers who ALREADY had a subscription was dark: the\n * in-place switch never opens a Checkout and never mints a new\n * subscription, so nothing in the revenue taxonomy saw it. Upgrades are the\n * half of AGL-1859 §2 that is supposed to be frictionless, and an\n * unmeasured half cannot be shown to be.\n */\n plan_upgraded: {\n from_plan: string\n to_plan: string\n interval: string\n }\n}\n\nexport type AnalyticsEventName = keyof AnalyticsEventParams\n\n/**\n * Where a sanitized event goes. The console registers a Firebase\n * `logEvent` transport; the tenant runtime and the plugin bundles have none\n * and fall through to `window.gtag`, which only exists once consent has been\n * granted.\n */\nexport type AnalyticsTransport = (\n name: AnalyticsEventName,\n params: Record<string, unknown>,\n) => void | Promise<void>\n\nlet configuredTransport: AnalyticsTransport | null = null\n\n/**\n * Register the transport for this surface. The console calls this once, with\n * Firebase's `logEvent`, because the console's GA is Firebase-initialised and\n * its `user_id`/user-property state lives on the Firebase Analytics instance —\n * poking `window.gtag` directly there would emit hits that miss it.\n *\n * The tenant runtime deliberately does NOT call this: the plugin bundles run\n * in their own realm and do not share this module instance with the host app,\n * so a module-scope singleton would be invisible to exactly the call sites\n * that need it (the form and newsletter elements). `window.gtag` is the only\n * thing genuinely shared across that boundary, and it is also the consent\n * gate, which makes the fallback the correct primary path there rather than a\n * degraded one.\n */\nexport function configureAnalyticsTransport(\n transport: AnalyticsTransport | null,\n): void {\n configuredTransport = transport\n}\n\n/** Test seam — drops the registered transport. */\nexport function resetAnalyticsTransport(): void {\n configuredTransport = null\n}\n\n/**\n * Param keys that must never reach GA, matched EXACTLY. Substring matching\n * would be wrong in both directions: it would drop the legitimate\n * `form_name` / `item_name` / `link_domain`, and it would still miss a\n * creatively-named new one. The value scan below is the backstop for those.\n */\nconst DENIED_PARAM_KEYS: ReadonlySet<string> = new Set([\n 'email',\n 'email_address',\n 'user_email',\n 'name',\n 'full_name',\n 'first_name',\n 'last_name',\n 'user_name',\n 'username',\n 'customer_name',\n 'org_name',\n 'organization_name',\n 'company',\n 'company_name',\n 'phone',\n 'phone_number',\n 'address',\n 'street',\n 'postal_code',\n 'zip',\n 'ip',\n 'ip_address',\n])\n\n/** Deliberately loose — this is a \"does it smell like an address\" test. */\nconst EMAIL_SHAPED = /[^\\s@]+@[^\\s@]+\\.[^\\s@]+/\n\n/**\n * GA4 truncates param values at 100 chars anyway; do it ourselves, visibly.\n *\n * Exported because an AUTHOR types some of these (the `trackGaEvent` step's\n * parameters), and the field they type into caps its input at the same number\n * — the truncation point and the affordance that describes it have to be one\n * value, or the editor promises a length the delivery quietly shortens.\n */\nexport const ANALYTICS_PARAM_MAX_LENGTH = 100\n\nfunction scrubValue(value: string): string | null {\n let candidate = value\n if (/^https?:\\/\\//i.test(candidate)) {\n try {\n const url = new URL(candidate)\n // Origin + pathname only: a query string is where a session token, a\n // signup email or a Stripe id ends up, and none of them belong in GA.\n candidate = `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n }\n // AFTER the URL reduction, not before. A page URL routinely carries an\n // address in its query (`?email=…` on a prefilled signup link), and testing\n // the raw string would throw the whole URL away for PII that the reduction\n // was about to remove — losing the legitimate page dimension to protect\n // something already protected. Testing the REDUCED value still catches an\n // address embedded in the path itself, which the reduction keeps.\n if (EMAIL_SHAPED.test(candidate)) return null\n return candidate.slice(0, ANALYTICS_PARAM_MAX_LENGTH)\n}\n\n/**\n * Strip anything identity-bearing from an event payload. Exported so\n * `analytics-events.spec.ts` can assert the guarantee directly rather than\n * only through `trackEvent`.\n */\nexport function sanitizeEventParams(\n params: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const safe: Record<string, unknown> = {}\n if (!params) return safe\n for (const [key, value] of Object.entries(params)) {\n if (DENIED_PARAM_KEYS.has(key.toLowerCase())) continue\n if (value === undefined || value === null) continue\n if (typeof value === 'string') {\n const scrubbed = scrubValue(value)\n if (scrubbed === null || scrubbed === '') continue\n safe[key] = scrubbed\n continue\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n safe[key] = value\n continue\n }\n if (Array.isArray(value)) {\n // `items` — sanitize each entry with the same rules.\n safe[key] = value.map((entry) =>\n entry && typeof entry === 'object'\n ? sanitizeEventParams(entry as Record<string, unknown>)\n : entry,\n )\n continue\n }\n if (typeof value === 'object') {\n safe[key] = sanitizeEventParams(value as Record<string, unknown>)\n }\n // Anything else (function, symbol) is dropped.\n }\n return safe\n}\n\n/**\n * Fire a GA4 event.\n *\n * Never throws and never queues. If the surface has no transport and no\n * `window.gtag` — which on a tenant site means the visitor has not granted\n * analytics consent — the event is DROPPED, permanently. See the module\n * comment for why a queue would be the wrong answer.\n */\nexport function trackEvent<K extends AnalyticsEventName>(\n name: K,\n params: AnalyticsEventParams[K],\n): void {\n deliver(name, sanitizeEventParams(params as Record<string, unknown>))\n}\n\n/**\n * How long a navigation may be held waiting for a hit to reach gtag. Short\n * enough to be invisible next to a Stripe redirect, long enough to cover a\n * Firebase Analytics init that has not settled yet.\n */\nconst NAVIGATION_FLUSH_TIMEOUT_MS = 300\n\n/**\n * Fire a GA4 event and resolve once it has been HANDED TO gtag — for the call\n * sites that navigate away in the same tick (AGL-1580).\n *\n * ## What was actually losing the event\n *\n * Not the transport, which was the obvious suspect and the wrong one. Measured\n * against real gtag.js with every transport interposed: gtag flushes its queue\n * on pagehide through `fetch(..., { keepalive: true })`, which is precisely the\n * mechanism that survives a document teardown. Once a hit reaches gtag, a\n * navigation does not destroy it — and `transport_type: 'beacon'`, the standard\n * answer, therefore fixes nothing here.\n *\n * What is lost is the hit that never REACHES gtag. Firebase's `logEvent` is\n * `async` and awaits the SDK's initialization promise before calling gtag\n * (`@firebase/analytics/dist/index.cjs.js`, `logEvent$1`). While that promise is\n * already settled the continuation is a microtask, microtasks drain before the\n * queued navigation task, and the hit gets out. While it is still PENDING — the\n * first checkout of a fresh session, exactly the case that has never yet been\n * seen in the property — the continuation is scheduled behind the navigation\n * and never runs at all. Measured both ways: pending init loses the event,\n * awaiting it delivers it, nothing else changed.\n *\n * ## Why a timeout rather than a bare await\n *\n * A bare `await` on `logEvent` hands the user's redirect to the analytics\n * stack. When the analytics host is blocked — an ad blocker, a corporate proxy,\n * a consent tool that never loads — Firebase's initialization promise can stay\n * pending indefinitely, and the checkout would hang on a metric. So the wait is\n * RACED against {@link NAVIGATION_FLUSH_TIMEOUT_MS}: a blocked analytics stack\n * costs the redirect 300ms once and then it proceeds, which is the same outcome\n * the caller had before this existed. Never rejects, for the same reason\n * {@link trackEvent} never throws.\n *\n * On a surface with no registered transport — the tenant runtime and the plugin\n * bundles, which go straight to `window.gtag` — delivery is synchronous, so\n * this resolves immediately and costs the storefront checkout nothing at all.\n */\nexport async function trackEventBeforeNavigation<K extends AnalyticsEventName>(\n name: K,\n params: AnalyticsEventParams[K],\n): Promise<void> {\n const delivered = deliver(\n name,\n sanitizeEventParams(params as Record<string, unknown>),\n )\n // Synchronous transport (or none): already handed over, nothing to wait for.\n if (!delivered || typeof delivered.then !== 'function') return\n await Promise.race([\n // A transport that REJECTS must not become an unhandled rejection, and\n // must not hold the navigation either — it has already failed.\n Promise.resolve(delivered).catch((): void => undefined),\n new Promise<void>((resolve) =>\n setTimeout(resolve, NAVIGATION_FLUSH_TIMEOUT_MS),\n ),\n ])\n}\n\n/**\n * Build the ONE `begin_checkout` payload, for every surface that starts a\n * checkout (AGL-1591).\n *\n * ## Why a constructor and not just the type\n *\n * Two surfaces fire this name: the console, when a plan checkout starts, and a\n * tenant storefront, when a cart checks out. Until AGL-1591 the storefront\n * fired it raw and carried `value`/`currency` only, so ONE event name arrived\n * in two shapes — and a `begin_checkout` breakdown showed two populations that\n * could not be compared, with the storefront half missing the `items` the GA4\n * ecommerce funnel is built on. Routing both through {@link trackEvent} makes\n * the compiler settle the KEYS.\n *\n * It does not settle the NUMBER, which is the other way two call sites of one\n * event diverge, and the more dangerous one because nothing about it looks\n * wrong: the console's annual plans are priced per-month-billed-yearly, so its\n * `value` is twelve of them, and that only stayed right because a comment said\n * so. Here `value` DERIVES from the items unless a caller states a different\n * one — a cart states its subtotal, which is authoritative after discounts —\n * so \"what the customer is about to be charged\" has one definition rather than\n * one per surface.\n *\n * Server-safe: pure, no DOM, so the Measurement Protocol sender can compose\n * the matching `purchase` items from the same shapes.\n */\nexport function buildBeginCheckoutParams(input: {\n items: AnalyticsItem[]\n /**\n * The amount actually being charged. Defaults to the sum of the items'\n * `price * quantity`, which is right whenever nothing has adjusted it.\n */\n value?: number\n /** ISO 4217. Defaults to `USD`, the only currency either surface bills in. */\n currency?: string\n /** `monthly` | `annual`. Subscriptions only — a storefront cart has none. */\n billingInterval?: string\n}): AnalyticsEventParams['begin_checkout'] {\n return {\n ...priceItems(input),\n ...(input.billingInterval ? { billing_interval: input.billingInterval } : {}),\n }\n}\n\n/**\n * The `currency`/`value`/`items` triple every GA4 ecommerce step shares.\n *\n * Private, because a caller should reach for the named builder for the event\n * it is about to send: the names differ in what they mean by `value` even\n * though the arithmetic is identical, and the JSDoc on each is where that is\n * written down.\n */\nfunction priceItems(input: {\n items: AnalyticsItem[]\n value?: number\n currency?: string\n}): { currency: string; value: number; items: AnalyticsItem[] } {\n const items = input.items ?? []\n const summed = items.reduce(\n (total, item) => total + (item.price ?? 0) * (item.quantity ?? 1),\n 0,\n )\n return {\n currency: input.currency ?? 'USD',\n // Money, so two decimals: a float sum of cents-derived prices produces\n // `59.99999999999999`, and GA would report that verbatim.\n value: Math.round((input.value ?? summed) * 100) / 100,\n items,\n }\n}\n\n/**\n * Build `view_cart` — the shopper is looking at the cart's contents.\n *\n * `value` is the cart's subtotal as the SERVER priced it, which is what makes\n * this comparable with the `begin_checkout` the same cart sends moments later:\n * two steps of one funnel that disagreed about the size of the same cart would\n * read as shoppers editing it between screens.\n */\nexport function buildViewCartParams(input: {\n items: AnalyticsItem[]\n value?: number\n currency?: string\n}): AnalyticsEventParams['view_cart'] {\n return priceItems(input)\n}\n\n/**\n * Build `add_to_cart` — one product going in, not the cart's new total.\n *\n * `value` is what was JUST ADDED (`price * quantity`), which is GA4's\n * definition and the only one that makes the metric additive: summing a\n * running cart total over a session would count the first item once per\n * subsequent add.\n *\n * Callers that cannot price the line — a quick-add with no resolved variant —\n * pass items alone and the pair is omitted rather than reported as zero. A\n * zero here would read as a free product in the merchant's report.\n */\nexport function buildAddToCartParams(input: {\n items: AnalyticsItem[]\n value?: number\n currency?: string\n}): AnalyticsEventParams['add_to_cart'] {\n const priced = priceItems(input)\n if (!(priced.value > 0)) return { items: priced.items }\n return priced\n}\n\n/**\n * Decide `site_published`'s `first_publish` from the host's routing map as it\n * stood BEFORE the route being published was registered (AGL-1588).\n *\n * ## What \"first\" means, and why it is defined once\n *\n * Three call sites report `site_published` — `publishScreenRoute`, the\n * besigner's one-click publish, and the scheduled publish executor, the last\n * of which is server-side and sends over the Measurement Protocol. A\n * breakdown is only worth registering if all three mean the same thing by it,\n * and \"first\" has several plausible readings. This is the one they share:\n *\n * **The host had no live route at all before this one.** Not \"first for the\n * org\" — that needs a cross-host query the server path cannot make, and the\n * scheduled sender's client id is derived from the HOST anyway, so an org is\n * not a thing it can see. Not \"first for this screen\" either, which would be\n * true of every second page a site adds and would make the dimension a\n * synonym for the event.\n *\n * So the metric it separates is the GTM §6 activation one: `first_publish:\n * true` counts sites that came alive, where the event alone counts publishes.\n *\n * ## The one dishonesty, and why it does not matter\n *\n * Unpublishing every route and publishing again reports `true` a second time.\n * Detecting that needs publish history the routing map does not keep. It is\n * harmless for the metric it exists for, because activation is read as the\n * share of USERS who ever sent `first_publish: true`, and a user counted twice\n * is still one user.\n *\n * Callers pass what they already hold — the live-subscribed map in the\n * console, a read snapshot on the server — and never a map read back AFTER\n * the write, which is never empty.\n *\n * ## The placeholder home page is not a publish (AGL-3408)\n *\n * Every new site is created with a home page already routed at `/`, so read\n * literally the map is never empty and no site would ever report a first\n * publish. The host's `defaultHomeScreenId` names that placeholder, and its\n * entry is not counted: the site \"came alive\" when its owner put something on\n * it, not when the platform did. Republishing the placeholder itself after\n * editing it counts, which is the same act.\n */\nexport function isFirstPublishedRoute(\n routing: Record<string, unknown> | null | undefined,\n defaultHomeScreenId?: string | null,\n): boolean {\n return !Object.keys(routing ?? {}).some(\n (screenId) => screenId !== defaultHomeScreenId,\n )\n}\n\n/**\n * The one delivery path, shared by {@link trackEvent} and\n * {@link trackAuthoredEvent}. Takes an ALREADY-sanitized payload — every\n * caller sanitizes first, which is what keeps \"a new call site cannot forget\"\n * true of the authored path too.\n */\nfunction deliver(\n name: string,\n safe: Record<string, unknown>,\n): void | Promise<void> {\n try {\n if (configuredTransport) {\n // The transport's name parameter is the taxonomy union, which an\n // authored name is by definition outside. Nominal only: the console is\n // the sole surface that registers one and it has no authored events\n // (the interaction runtime is tenant-side), and Firebase `logEvent`\n // takes an arbitrary string regardless.\n // Returned, not discarded (AGL-1580). A transport may be ASYNC —\n // Firebase's `logEvent` awaits the SDK's initialization promise before it\n // reaches gtag at all — and a caller that is about to navigate has to be\n // able to wait for it. See `trackEventBeforeNavigation`.\n return configuredTransport(name as AnalyticsEventName, safe)\n }\n if (typeof window === 'undefined') return\n const gtag = (window as unknown as { gtag?: unknown }).gtag\n if (typeof gtag !== 'function') return\n // Synchronous, so nothing is returned and nothing needs awaiting: by the\n // time this call has returned, gtag.js already holds the hit.\n ;(gtag as (...args: unknown[]) => void)('event', name, safe)\n } catch {\n // Analytics never breaks the page — the same posture as the error beacon\n // and the pageview beacon.\n }\n}\n\n/**\n * The taxonomy's names at RUN time. A `Record<AnalyticsEventName, true>` rather\n * than a hand-kept array so the compiler enforces both directions: adding an\n * event to {@link AnalyticsEventParams} without adding it here is a missing-key\n * error, and a name here that is not in the taxonomy is an excess-property one.\n *\n * It exists for {@link trackAuthoredEvent}, which has to refuse these names —\n * a drifting copy would silently re-open the collision it is here to close.\n */\nconst TAXONOMY_EVENT_NAMES: Record<AnalyticsEventName, true> = {\n sign_up: true,\n login: true,\n generate_lead: true,\n select_content: true,\n org_created: true,\n host_created: true,\n site_published: true,\n stripe_connected: true,\n begin_checkout: true,\n purchase: true,\n view_item: true,\n add_to_cart: true,\n view_cart: true,\n aglyn_overlay: true,\n aglyn_experiment: true,\n click: true,\n assistant_message_sent: true,\n assistant_feedback: true,\n assistant_proposal_shown: true,\n assistant_proposal_confirmed: true,\n ai_job_completed: true,\n ai_job_failed: true,\n churn_survey_submitted: true,\n downsell_accepted: true,\n winback_discount_accepted: true,\n cancellation_completed: true,\n plan_downgrade_scheduled: true,\n plan_upgraded: true,\n}\n\n/** The taxonomy, enumerable. */\nexport const ANALYTICS_EVENT_NAMES = Object.keys(\n TAXONOMY_EVENT_NAMES,\n) as AnalyticsEventName[]\n\n/**\n * Event names WE send that this module's union cannot hold, because nothing\n * client-side ever fires them: they are emitted only by the Measurement\n * Protocol sender (`ga4-measurement-protocol.ts`), from a Stripe webhook.\n *\n * They still have to be RESERVED against authored names, and the reason is\n * the same one that puts `purchase` in the union — only less obvious, which\n * is why it was missed. `aglyn.com` is itself a tenant site, pointed at the\n * platform measurement id (`site-analytics.tsx`), so an authored\n * `trackGaEvent` step on our own marketing site lands in the SAME property as\n * these server hits. An authored `refund` does not merely add noise: GA4\n * treats `refund` as ecommerce and SUBTRACTS its `value` from reported\n * revenue, so a mistyped step could walk real money out of the report — the\n * `purchase` hazard, running in the direction nobody audits.\n *\n * Kept as a separate list rather than folded into {@link AnalyticsEventParams}\n * on purpose: adding them to the union would give {@link trackEvent} a\n * client-side door to events that must only ever come from the server, where\n * the authoritative money is. A name here is ours, is never sent from a\n * browser, and is never available to an author.\n *\n * This list is the reason \"not in {@link ANALYTICS_EVENT_NAMES}\" is NOT on its\n * own a sound test for \"authored\" — use {@link isReservedAnalyticsEventName}.\n */\nconst SERVER_ONLY_EVENT_NAMES: ReadonlySet<string> = new Set([\n 'refund',\n 'subscription_cancelled',\n])\n\n/**\n * Every event name Aglyn itself sends, from any surface — the union plus the\n * server-only names above. Exported so a caller can ask the question the two\n * separate lists no longer answer alone.\n */\nexport function isReservedAnalyticsEventName(name: string): boolean {\n return (\n TAXONOMY_EVENT_NAMES[name as AnalyticsEventName] === true ||\n SERVER_ONLY_EVENT_NAMES.has(name)\n )\n}\n\n/**\n * GA4's own reserved event names — GA drops a hit that uses one, so sending it\n * is not pollution but silence, which is the worse failure of the two because\n * nothing anywhere says so.\n */\nconst GA4_RESERVED_EVENT_NAMES: ReadonlySet<string> = new Set([\n 'ad_activeview',\n 'ad_click',\n 'ad_exposure',\n 'ad_impression',\n 'ad_query',\n 'ad_reward',\n 'adunit_exposure',\n 'app_background',\n 'app_clear_data',\n 'app_exception',\n 'app_install',\n 'app_remove',\n 'app_store_refund',\n 'app_store_subscription_cancel',\n 'app_store_subscription_convert',\n 'app_store_subscription_renew',\n 'app_update',\n 'app_upgrade',\n 'dynamic_link_app_open',\n 'dynamic_link_app_update',\n 'dynamic_link_first_open',\n 'error',\n 'first_open',\n 'first_visit',\n 'in_app_purchase',\n 'notification_dismiss',\n 'notification_foreground',\n 'notification_open',\n 'notification_receive',\n 'os_update',\n 'screen_view',\n 'session_start',\n 'user_engagement',\n])\n\n/** GA4 reserves these prefixes outright, whatever follows them. */\nconst GA4_RESERVED_PREFIXES = ['firebase_', 'google_', 'ga_'] as const\n\n/** GA4's hard limit on an event name. Over it, GA drops the event. */\nconst MAX_EVENT_NAME_LENGTH = 40\n\n/**\n * The outcome of putting an authored name through GA4's rules, so the\n * interaction builder can say WHY it refused a name and the runtime can drop\n * the event for the same reason.\n */\nexport interface ResolvedAuthoredEventName {\n /** The name to send, or null when the event must not be sent at all. */\n name: string | null\n /**\n * `reserved` — collides with the taxonomy or with GA4's own names.\n * `unusable` — nothing survives normalization (empty, or no leading letter).\n */\n reason?: 'reserved' | 'unusable'\n}\n\n/**\n * Put an authored event name through GA4's naming rules and our own.\n *\n * Normalization is forgiving on purpose: `\"CTA Click!\"` becomes `cta_click`\n * and still reports, where GA would have dropped it. Names already sitting in\n * published sites were never validated, so refusing them outright would delete\n * working metrics from a paying customer's property to fix a formatting nit.\n *\n * Collisions, in contrast, are refused rather than rewritten. On a tenant site\n * the authored events and OUR events (`generate_lead` from the form element,\n * `select_content`/`click` from the link listener) land in the same property,\n * so an authored `purchase` does not merely add noise — it mixes hand-authored\n * hits into a real revenue number. Refusing is also what keeps authored events\n * separable in reports: an event that is not one of {@link ANALYTICS_EVENT_NAMES}\n * is, by construction, authored.\n *\n * Deliberately NOT prefixed (`site_*`) to achieve that separation. A prefix\n * would rename events already flowing into customers' properties and break\n * every report and key-event conversion configured on the old name — a\n * migration cost paid by people who did nothing wrong.\n */\nexport function resolveAuthoredEventName(\n raw: string | undefined | null,\n): ResolvedAuthoredEventName {\n const normalized = String(raw ?? '')\n .trim()\n .toLowerCase()\n // Anything GA4 does not allow in a name becomes an underscore...\n .replace(/[^a-z0-9_]+/g, '_')\n // ...and a name must START with a letter, so drop what precedes one.\n .replace(/^[^a-z]+/, '')\n .replace(/_{2,}/g, '_')\n .slice(0, MAX_EVENT_NAME_LENGTH)\n // Truncation can leave a trailing underscore; so can the substitution.\n .replace(/_+$/, '')\n if (!normalized) return { name: null, reason: 'unusable' }\n if (\n isReservedAnalyticsEventName(normalized) ||\n GA4_RESERVED_EVENT_NAMES.has(normalized) ||\n GA4_RESERVED_PREFIXES.some((prefix) => normalized.startsWith(prefix))\n ) {\n return { name: null, reason: 'reserved' }\n }\n return { name: normalized }\n}\n\n/** One warning per distinct name per page load — an `everyTime` automation\n * would otherwise fill the console with the same line. */\nconst warnedAuthoredNames = new Set<string>()\n\n/**\n * Fire an event whose name and params were written by a SITE AUTHOR, not by a\n * developer — today only the `trackGaEvent` action step (AGL-1587).\n *\n * Same consent gate, same sanitizer, same drop-never-queue posture as\n * {@link trackEvent}; the only difference is that the name is checked at run\n * time instead of by the compiler, because there is no compiler between the\n * interaction builder and here.\n *\n * A refused event is dropped and warned about in the console rather than\n * surfaced in the page. Nothing here can reach the author — the code is\n * running for a VISITOR of their site, and turning the author's configuration\n * mistake into something a visitor sees would be a worse bug than the missing\n * metric. The author-facing half lives in `validateHostAction`, which refuses\n * to save a name this function would refuse to send, so a silent drop should\n * only ever happen to a step authored before AGL-1587.\n */\nexport function trackAuthoredEvent(\n name: string | undefined | null,\n params?: Record<string, unknown> | null,\n): void {\n const resolved = resolveAuthoredEventName(name)\n if (!resolved.name) {\n const key = String(name ?? '')\n if (!warnedAuthoredNames.has(key)) {\n warnedAuthoredNames.add(key)\n try {\n console.warn(\n `[aglyn] analytics: the event \"${key}\" was not sent — ` +\n (resolved.reason === 'reserved'\n ? 'that name is reserved. Rename the step in the interaction builder.'\n : 'an event name must start with a letter.'),\n )\n } catch {\n // A console that throws is still not worth breaking the page for.\n }\n }\n return\n }\n deliver(resolved.name, sanitizeEventParams(params ?? undefined))\n}\n\n/** Test seam — forgets which authored names have already been warned about. */\nexport function resetAuthoredEventWarnings(): void {\n warnedAuthoredNames.clear()\n}\n"],"names":["readGaClientId","measurementId","Promise","resolve","window","gtag","settled","finish","value","setTimeout","id","configuredTransport","configureAnalyticsTransport","transport","resetAnalyticsTransport","DENIED_PARAM_KEYS","Set","EMAIL_SHAPED","ANALYTICS_PARAM_MAX_LENGTH","scrubValue","candidate","test","url","URL","origin","pathname","slice","sanitizeEventParams","params","safe","key","Object","entries","has","toLowerCase","undefined","scrubbed","Array","isArray","map","entry","trackEvent","name","deliver","NAVIGATION_FLUSH_TIMEOUT_MS","trackEventBeforeNavigation","delivered","then","race","catch","buildBeginCheckoutParams","input","priceItems","billingInterval","billing_interval","items","summed","reduce","total","item","price","quantity","currency","Math","round","buildViewCartParams","buildAddToCartParams","priced","isFirstPublishedRoute","routing","defaultHomeScreenId","keys","some","screenId","TAXONOMY_EVENT_NAMES","sign_up","login","generate_lead","select_content","org_created","host_created","site_published","stripe_connected","begin_checkout","purchase","view_item","add_to_cart","view_cart","aglyn_overlay","aglyn_experiment","click","assistant_message_sent","assistant_feedback","assistant_proposal_shown","assistant_proposal_confirmed","ai_job_completed","ai_job_failed","churn_survey_submitted","downsell_accepted","winback_discount_accepted","cancellation_completed","plan_downgrade_scheduled","plan_upgraded","ANALYTICS_EVENT_NAMES","SERVER_ONLY_EVENT_NAMES","isReservedAnalyticsEventName","GA4_RESERVED_EVENT_NAMES","GA4_RESERVED_PREFIXES","MAX_EVENT_NAME_LENGTH","resolveAuthoredEventName","raw","normalized","String","trim","replace","reason","prefix","startsWith","warnedAuthoredNames","trackAuthoredEvent","resolved","add","console","warn","resetAuthoredEventWarnings","clear"],"mappings":"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"}
|