@aglyn/plugins-marketing 1.0.0-beta.218 → 1.0.0-beta.220
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/README.md +5 -1
- package/package.json +16 -16
- package/src/lib/analytics-provider.d.ts +177 -0
- package/src/lib/analytics-provider.js +370 -0
- package/src/lib/analytics-provider.js.map +1 -0
- package/src/lib/components/besigner-section-experiments.d.ts +20 -0
- package/src/lib/components/besigner-section-experiments.js +128 -0
- package/src/lib/components/besigner-section-experiments.js.map +1 -0
- package/src/lib/components/campaign-conversions-card.js +1 -1
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-detail-card.js +16 -16
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-members-section.js +15 -14
- package/src/lib/components/campaign-members-section.js.map +1 -1
- package/src/lib/components/campaign-reach-sections.js +4 -1
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/campaign-report-card.js +8 -6
- package/src/lib/components/campaign-report-card.js.map +1 -1
- package/src/lib/components/campaigns-card.js +1 -1
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/conversion-attribution.component.d.ts +1 -1
- package/src/lib/components/conversion-attribution.component.js +2 -2
- package/src/lib/components/conversion-attribution.component.js.map +1 -1
- package/src/lib/components/email-detail.js +12 -7
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/email-messages-widget.js +1 -1
- package/src/lib/components/email-messages-widget.js.map +1 -1
- package/src/lib/components/emails-list-card.js +11 -5
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +12 -7
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/org-experiments-card.js +1 -1
- package/src/lib/components/org-experiments-card.js.map +1 -1
- package/src/lib/components/record-attribution-widget.js +1 -1
- package/src/lib/components/record-attribution-widget.js.map +1 -1
- package/src/lib/components/record-route-context.d.ts +41 -0
- package/src/lib/components/record-route-context.js +63 -0
- package/src/lib/components/record-route-context.js.map +1 -0
- package/src/lib/components/site-runtime.js +2 -2
- package/src/lib/components/site-runtime.js.map +1 -1
- package/src/lib/components/use-marketing-hub-path.d.ts +18 -5
- package/src/lib/components/use-marketing-hub-path.js +36 -6
- package/src/lib/components/use-marketing-hub-path.js.map +1 -1
- package/src/lib/constants/operator-alerts.js +4 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.server.d.ts +3 -1
- package/src/lib/declarations.server.js +17 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/campaign-conversions.d.ts +301 -0
- package/src/lib/model/campaign-conversions.js +252 -0
- package/src/lib/model/campaign-conversions.js.map +1 -0
- package/src/lib/model/campaign-kind.d.ts +50 -0
- package/src/lib/model/campaign-kind.js +52 -0
- package/src/lib/model/campaign-kind.js.map +1 -0
- package/src/lib/model/campaign-members-query.js +1 -1
- package/src/lib/model/campaign-members-query.js.map +1 -1
- package/src/lib/model/campaign-revenue.d.ts +327 -0
- package/src/lib/model/campaign-revenue.js +332 -0
- package/src/lib/model/campaign-revenue.js.map +1 -0
- package/src/lib/model/campaign-send-time.d.ts +74 -0
- package/src/lib/model/campaign-send-time.js +120 -0
- package/src/lib/model/campaign-send-time.js.map +1 -0
- package/src/lib/model/experiments.js +1 -1
- package/src/lib/model/experiments.js.map +1 -1
- package/src/lib/model/index.d.ts +3 -0
- package/src/lib/model/index.js +4 -1
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/overlay-record-list.d.ts +33 -0
- package/src/lib/model/overlay-record-list.js +53 -0
- package/src/lib/model/overlay-record-list.js.map +1 -0
- package/src/lib/plugin.js +12 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/campaign-batch-plan.d.ts +73 -0
- package/src/lib/server/campaign-batch-plan.js +79 -0
- package/src/lib/server/campaign-batch-plan.js.map +1 -0
- package/src/lib/server/campaign-manage.js +19 -60
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-send.d.ts +1 -1
- package/src/lib/server/campaign-send.js +32 -11
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/email-events.js +16 -16
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/list-send-time-facts.d.ts +60 -0
- package/src/lib/server/list-send-time-facts.js +108 -0
- package/src/lib/server/list-send-time-facts.js.map +1 -0
- package/src/lib/server/overlay-beacon.d.ts +46 -0
- package/src/lib/server/overlay-beacon.js +76 -0
- package/src/lib/server/overlay-beacon.js.map +1 -0
- package/src/lib/server.js +4 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.d.ts +29 -0
- package/src/lib/tenant-emails.js +41 -0
- package/src/lib/tenant-emails.js.map +1 -0
- package/src/lib/testing/stand-in-emails-page-routes.d.ts +27 -0
- package/src/lib/testing/stand-in-emails-page-routes.js +42 -0
- package/src/lib/testing/stand-in-emails-page-routes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -58
- package/src/lib/components/use-emails-hub-path.js +0 -109
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
package/README.md
CHANGED
|
@@ -16,18 +16,22 @@ The plugin declares four registrars in `plugins.config.json`, plus a `site` modu
|
|
|
16
16
|
|
|
17
17
|
**On a published site** (`registerMarketingPlugin`, the `site` registrar, exported from `@aglyn/plugins-marketing/site`). It registers one site runtime, `marketing-site-runtime`, with `registerSiteRuntime`. The runtime is not a canvas element: the tenant page renders it generically, and it draws the active announcement bars and popups, applies experiment variants and runs the page's client automations from the data the server enricher wrote. In the editor's Preview, where no enricher runs, it rebuilds that data on the client through a deferred import.
|
|
18
18
|
|
|
19
|
+
**Analytics tags** (`analyticsProvider`, exported from `@aglyn/plugins-marketing/analytics-provider`). The Google tag adapter behind core's analytics-provider contract: the Google Analytics 4 pair and the Google Tag Manager pair a site configures in its analytics settings (`gaMeasurementId`, `gtmContainerId`), with the Consent Mode default in front of each; how a resident Google tag is told a visitor's answer changed (the `ga-disable-<id>` flag and a consent `update`); and how an event reaches it. Declared under `analyticsProvider` in `plugins.config.json`, so core knows which settings configure a tag without loading it, and loaded only by a published page whose site configures one and by the console, whose own tag the consent controls answer to.
|
|
20
|
+
|
|
19
21
|
**Console** (`registerMarketingConsole`, the `console` registrar):
|
|
20
22
|
|
|
21
23
|
- A `Marketing` nav item at `/marketing` with the sections Overview, Campaigns, Conversions, Overlays and A/B testing. Each section is a route; the page is code-split.
|
|
22
24
|
- The same page at the organization level, `/[orgSlug]/marketing`, declared in `orgNavItems` with the site's sections in the site's order. Campaigns and their sends belong to the organization (`orgs/{orgId}/emailCampaigns`, `orgs/{orgId}/campaigns`): the org hub lists every one, places a campaign on every site or on chosen sites (`visibleTo`), and asks which site an email is sent as before writing it; a site's hub lists the campaigns placed on that site. Overlays, A/B tests and conversions are each one site's, so the org hub reads them per site and bounded: Overview sums the org's sends and each site's overlay and test figures as server aggregates, for a capped number of sites; Overlays and A/B testing list every site's, one page of sites at a time, switch an overlay on or off, and send every edit to the site's own hub; Conversions shows the site picked under "Conversions on". Overlays and A/B testing carry their plan flags on the org sections, so an organization without the plan reads nothing per site. Both levels share the `release_marketing` flag. The organization's messages are its Emails page's; the old `/[orgSlug]/marketing/emails/…` addresses redirect there (`apps/console/next.config.js`).
|
|
23
25
|
- Campaigns end to end. The plugin owns the campaign composer, test sends, scheduling, reports and recipients. The individual messages are listed on the Emails page, but that page belongs to the email plugin, which hosts an `emailMessages` zone; this plugin fills it with the message list, one message's page and its composer. It also fills `emailTemplateRecipients` (who received the sends built from a template).
|
|
24
|
-
- Widgets in other hosts' slots: the "Last campaign" card on the site dashboard (`hostDashboard`), the campaigns card in the Inbox (`inboxCampaigns`),
|
|
26
|
+
- Widgets in other hosts' slots: the "Last campaign" card on the site dashboard (`hostDashboard`), the campaigns card in the Inbox (`inboxCampaigns`), campaign attribution on a record's page (`crmRecordAttribution`, `inboxRecordAttribution`), and the site's section experiments, reported to the besigner's Interactions section (`besignerInteractions`), which badges an element with its experiment and starts a draft on a page's element.
|
|
25
27
|
- Zones this plugin hosts for other plugins to fill, registered with `registerPluginZone`: two on the A/B testing card (beside the variants, and below a test's results), and five bare zones in the campaign composer and on a message's page for whichever plugin keeps the mail itself: the topic picker, the topic options, the add-a-sender editor, the design creator and the sent-design preview.
|
|
26
28
|
- A record route for `campaign`, at both the site and the organization level, so another plugin can link to a campaign without knowing this plugin's URLs.
|
|
29
|
+
- A record list source for `overlay` (`plugin-record-lists`): a site's announcement bars and popups, named, for another plugin's picker — the automation editor's "Show overlay" step — to list without reading this plugin's collection.
|
|
27
30
|
|
|
28
31
|
**Server**, exported from `@aglyn/plugins-marketing/server`:
|
|
29
32
|
|
|
30
33
|
- `registerMarketingApi` (the `tenantApi` registrar): the `experiments/track` beacon, which counts exposures and conversions for a running experiment and can finish a test whose auto-winner rule is met, and a site page enricher (`registerSitePageEnricher`) that adds overlays, automations and experiments to a page's data.
|
|
34
|
+
- From its server declarations, the site collector's `overlay` beacons (`registerPluginSiteBeacon`): each bar and popup event the site runtime reports is counted into the site's day document and the overlay's own counters, after the collector's host and lockdown gates.
|
|
31
35
|
- `registerMarketingConsoleApi` (the `consoleApi` registrar): `campaigns/send`, `campaigns/manage`, `campaigns/recipients`, `campaigns/process-scheduled`, `lists/materialize`, and the delivery events webhook at `email/events`. It also registers the campaign draft writer on the core's resource-drafts seam and figure readers for campaign and experiment results.
|
|
32
36
|
|
|
33
37
|
**Exports from `.`**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-marketing",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.220",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.220",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.220",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.220",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.220",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.220",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.220",
|
|
34
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.220",
|
|
35
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.220",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.220",
|
|
37
|
+
"@aglyn/shared-util-http": "1.0.0-beta.220",
|
|
38
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.220",
|
|
39
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.220",
|
|
40
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.220",
|
|
41
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.220",
|
|
42
42
|
"@swc/helpers": "0.5.23"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@mui/x-data-grid": "^9.12.0",
|
|
47
47
|
"firebase": "^12.18.0",
|
|
48
48
|
"firebase-admin": "^14.3.0",
|
|
49
|
-
"next": "16.3.3",
|
|
49
|
+
"next": "^16.3.3",
|
|
50
50
|
"react": "^19.2.8"
|
|
51
51
|
},
|
|
52
52
|
"sideEffects": false,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { AnalyticsGrants, AnalyticsProvider } from '@aglyn/aglyn/app-utils/analytics-provider';
|
|
18
|
+
/**
|
|
19
|
+
* The `window` flag `gtag.js` consults before every hit: with
|
|
20
|
+
* `window['ga-disable-G-XXXX'] === true` the tag for that property sends
|
|
21
|
+
* nothing and writes nothing. Google's own documented opt-out mechanism, and
|
|
22
|
+
* the one that works on a tag which never received a consent-mode default
|
|
23
|
+
* (AGL-1608) — a case that still exists after AGL-1622, because a tag arriving
|
|
24
|
+
* through GTM or a host's own CMP is not one this adapter declared for.
|
|
25
|
+
*/
|
|
26
|
+
export declare const GA_DISABLE_FLAG_PREFIX = "ga-disable-";
|
|
27
|
+
/**
|
|
28
|
+
* The consent-mode `default` declaration that goes in front of the tag
|
|
29
|
+
* (AGL-1622), for injection into the same inline block that creates
|
|
30
|
+
* `dataLayer` and calls `gtag('config', …)`.
|
|
31
|
+
*
|
|
32
|
+
* ## What this is, and — load-bearing — what it is NOT
|
|
33
|
+
*
|
|
34
|
+
* It is NOT a licence to load the tag earlier. It is emitted INSIDE the block
|
|
35
|
+
* that only renders when the AGL-1498 gate has already said yes, so it is
|
|
36
|
+
* reached only on a pageview where analytics is granted. On every gated
|
|
37
|
+
* pageview — an EU/UK/EEA visitor with no explicit accept, an unknown region, a
|
|
38
|
+
* declined or opted-out visitor, a GPC browser — nothing here renders, no
|
|
39
|
+
* request goes to `googletagmanager.com`, and this constant is never evaluated
|
|
40
|
+
* into the page. "The script never LOADS, rather than load-then-suppress" is a
|
|
41
|
+
* stated AGL-1498 property and it is intact everywhere the gate applies;
|
|
42
|
+
* `consent-mode-default.spec.tsx` fails if that ever stops being true.
|
|
43
|
+
*
|
|
44
|
+
* the decision, 2026-08-14: load-then-restrict is approved for the UNITED
|
|
45
|
+
* STATES, where the implied-consent posture already permits the load and the
|
|
46
|
+
* restriction signals act on a tag that is legitimately resident. EU and UK
|
|
47
|
+
* keep the prior-consent gate exactly as it is, because loading an analytics
|
|
48
|
+
* tag before consent is the specific thing prior-consent law prohibits. So the
|
|
49
|
+
* consent-mode mechanism is strictly ADDITIVE to the gate, never a replacement
|
|
50
|
+
* for it.
|
|
51
|
+
*
|
|
52
|
+
* What declaring it actually buys, given the tag is granted by the time it is
|
|
53
|
+
* read:
|
|
54
|
+
*
|
|
55
|
+
* - The advertising signals are denied FROM THE FIRST HIT. Without a default,
|
|
56
|
+
* a freshly loaded tag runs with `ad_storage` unrestricted and only becomes
|
|
57
|
+
* denied if the visitor happens to withdraw and change their mind again
|
|
58
|
+
* (AGL-1608's `update`). The load-time state now matches the tool's actual
|
|
59
|
+
* scope instead of being wider than it.
|
|
60
|
+
* - A later `update` is a transition from a declared state rather than a tag's
|
|
61
|
+
* first-ever consent signal, which is the shape GA4's consent-mode reporting
|
|
62
|
+
* and any third-party CMP alongside ours both expect.
|
|
63
|
+
*
|
|
64
|
+
* The value is a literal, built from `analyticsConsentSignals` — no
|
|
65
|
+
* interpolated input reaches it, which matters because it lands inside an
|
|
66
|
+
* inline script (the AGL-138 concern).
|
|
67
|
+
*/
|
|
68
|
+
export declare const GA_CONSENT_DEFAULT_SNIPPET: string;
|
|
69
|
+
/**
|
|
70
|
+
* The same declaration for a visitor who granted ADVERTISING too (AGL-1649).
|
|
71
|
+
*
|
|
72
|
+
* A second CONSTANT rather than a parameterised builder, for the reason the
|
|
73
|
+
* first one is a constant: it lands inside an inline script (the AGL-138
|
|
74
|
+
* concern), and a constant cannot interpolate anything. Both values are
|
|
75
|
+
* literals fixed at module load; the only thing the caller chooses is which
|
|
76
|
+
* of the two to emit.
|
|
77
|
+
*
|
|
78
|
+
* Reached only where the page's `advertising` answer is yes — host opted in,
|
|
79
|
+
* explicit accept, explicit yes to this category — which is evaluated
|
|
80
|
+
* client-side after hydration, like every other part of this gate. The ISR
|
|
81
|
+
* property is unaffected: the cached HTML contains neither snippet, because
|
|
82
|
+
* the whole block is inside the client-side `analyticsAllowed` condition.
|
|
83
|
+
*
|
|
84
|
+
* Declaring the grant as the DEFAULT rather than sending a later `update` is
|
|
85
|
+
* what keeps the tag's first hit correct. An update would arrive after
|
|
86
|
+
* `config`, so the session's first pageview — usually the entire session for
|
|
87
|
+
* a marketing visit — would carry the denied state the visitor did not
|
|
88
|
+
* choose.
|
|
89
|
+
*/
|
|
90
|
+
export declare const GA_CONSENT_DEFAULT_WITH_ADS_SNIPPET: string;
|
|
91
|
+
/**
|
|
92
|
+
* Consent-mode URL passthrough for the platform's own tag (AGL-2548).
|
|
93
|
+
*
|
|
94
|
+
* The advertising signals in {@link GA_CONSENT_DEFAULT_SNIPPET} are denied, so
|
|
95
|
+
* on `aglyn.com` gtag writes no `_gcl_aw` cookie and the `gclid` an ad click
|
|
96
|
+
* arrives with lives only in that first URL. The conversions it should be
|
|
97
|
+
* credited for fire one hop later, on the console, where the posture grants
|
|
98
|
+
* advertising storage outside the prior-consent regions — and without this
|
|
99
|
+
* declaration the click id never makes that hop. The console reports a
|
|
100
|
+
* conversion with no click behind it, and Google Ads counts nothing.
|
|
101
|
+
*
|
|
102
|
+
* `url_passthrough` is Google's mechanism for exactly this state: while
|
|
103
|
+
* `ad_storage` is denied, gtag appends the click identifiers to links into the
|
|
104
|
+
* cross-domain-linked hosts as URL parameters instead of setting a cookie. The
|
|
105
|
+
* receiving tag reads them under its own consent state. Nothing is stored on
|
|
106
|
+
* the denying surface, so the marketing site's opt-in advertising posture is
|
|
107
|
+
* unchanged; the click id simply survives the link it was always meant to
|
|
108
|
+
* survive.
|
|
109
|
+
*
|
|
110
|
+
* `ads_data_redaction` is the companion Google documents alongside it: with
|
|
111
|
+
* `ad_storage` denied, the ad click identifiers are stripped from the hits this
|
|
112
|
+
* tag sends and its network requests go to a cookieless endpoint. Declared
|
|
113
|
+
* together so a denied visitor is measured with less, not merely differently.
|
|
114
|
+
*
|
|
115
|
+
* Both are `set` calls, so they must precede `gtag('config', …)` in the same
|
|
116
|
+
* inline block — a `set` applies to hits processed after it, and the session's
|
|
117
|
+
* first pageview is the one carrying the click.
|
|
118
|
+
*
|
|
119
|
+
* PLATFORM ID ONLY. A customer's tag is configured with their id and their
|
|
120
|
+
* consent posture; how their ad click ids travel is theirs to decide, and a
|
|
121
|
+
* passthrough declared on their behalf would rewrite links on their site.
|
|
122
|
+
*/
|
|
123
|
+
export declare const GA_CLICK_ID_PASSTHROUGH_SNIPPET: string;
|
|
124
|
+
/** The marker `advertising-tags.ts` names in `sharesLibrary` for gtag.js. */
|
|
125
|
+
export declare const GTAG_LIBRARY = "googletagmanager.com/gtag/js";
|
|
126
|
+
/**
|
|
127
|
+
* The measurement ids whose tag is actually RESIDENT in this page.
|
|
128
|
+
*
|
|
129
|
+
* Discovered from the page rather than taken from the host document on
|
|
130
|
+
* purpose: what has to be silenced is whatever gtag.js is currently loaded and
|
|
131
|
+
* configured for, which is not always what the host record configures — a tag
|
|
132
|
+
* can arrive through GTM, the console's is injected by its analytics SDK, and
|
|
133
|
+
* a stale `<script>` from earlier in the pageview is exactly the thing this
|
|
134
|
+
* function exists to find.
|
|
135
|
+
*
|
|
136
|
+
* Ids are format-checked before they are used, because each one becomes a
|
|
137
|
+
* `window` property name.
|
|
138
|
+
*/
|
|
139
|
+
export declare function residentGaMeasurementIds(): string[];
|
|
140
|
+
/**
|
|
141
|
+
* Make every resident Google tag agree with the visitor's answer, and return
|
|
142
|
+
* the measurement ids it acted on.
|
|
143
|
+
*
|
|
144
|
+
* ## Why deleting the cookies is not enough (AGL-1608)
|
|
145
|
+
*
|
|
146
|
+
* The AGL-1498 gate stops the script from LOADING, which is the right
|
|
147
|
+
* enforcement for a fresh pageview and the wrong tool for a visitor who
|
|
148
|
+
* withdraws mid-pageview: `gtag.js` has already executed, and React unmounting
|
|
149
|
+
* the `<script>` element does not unload it. `window.gtag` stays live, GA4
|
|
150
|
+
* enhanced measurement fires on its own (scroll depth, outbound click, file
|
|
151
|
+
* download), and the tag re-writes `_ga_<id>` AFTER the platform's cookie
|
|
152
|
+
* sweep has deleted it. Reproduced on aglyn.com: an opt-out, a hand-run sweep
|
|
153
|
+
* to zero cookies, then one scroll to the footer brought `_ga_YW5PG16YTM`
|
|
154
|
+
* back.
|
|
155
|
+
*
|
|
156
|
+
* Two signals, because they fail differently and neither is guaranteed:
|
|
157
|
+
* the `ga-disable-<id>` window flag silences a tag that never received a
|
|
158
|
+
* consent-mode default, and the `consent`/`update` call reaches a tag that
|
|
159
|
+
* arrived through GTM with its own id this function never saw.
|
|
160
|
+
*
|
|
161
|
+
* ## What this deliberately does NOT do
|
|
162
|
+
*
|
|
163
|
+
* It never touches the gate. Both signals act only on a tag that is already
|
|
164
|
+
* resident — which, by construction, only exists after a grant. The
|
|
165
|
+
* consent-mode DEFAULT ({@link GA_CONSENT_DEFAULT_SNIPPET}) is declared inside
|
|
166
|
+
* the gated block and therefore only on a pageview the gate already allowed.
|
|
167
|
+
*
|
|
168
|
+
* Symmetric on purpose: a visitor who opts out and changes their mind in the
|
|
169
|
+
* same pageview would otherwise stay silently unmeasured until they navigated,
|
|
170
|
+
* because the re-rendered `<Script>` cannot re-execute an already-loaded tag.
|
|
171
|
+
* The re-grant restores exactly what `grants` says, and core clamps
|
|
172
|
+
* advertising to analytics before it arrives, so a withdrawal can never
|
|
173
|
+
* restore advertising alone.
|
|
174
|
+
*/
|
|
175
|
+
export declare function applyGoogleTagConsent(grants: AnalyticsGrants): string[];
|
|
176
|
+
/** The adapter core registers from each app's generated manifest. */
|
|
177
|
+
export declare const analyticsProvider: AnalyticsProvider;
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { analyticsEnvironmentForcesInternal } from "@aglyn/aglyn/app-utils/analytics-environment";
|
|
18
|
+
import { INTERNAL_TRAFFIC_FORCED_SNIPPET, INTERNAL_TRAFFIC_GTAG_SNIPPET } from "@aglyn/aglyn/app-utils/internal-traffic";
|
|
19
|
+
import { PLATFORM_GA_MEASUREMENT_ID } from "@aglyn/aglyn/app-utils/platform-marketing-host";
|
|
20
|
+
import { analyticsConsentSignals, consentModeSignals, GA_MEASUREMENT_ID_PATTERN, resolveGaMeasurementId, resolveGtmContainerId } from "@aglyn/aglyn/app-utils/visitor-consent";
|
|
21
|
+
/** The resident `gtag`, or null when no Google tag is on the page. */ function residentGtag() {
|
|
22
|
+
if (typeof window === 'undefined') return null;
|
|
23
|
+
const gtag = window.gtag;
|
|
24
|
+
return typeof gtag === 'function' ? gtag : null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The `window` flag `gtag.js` consults before every hit: with
|
|
28
|
+
* `window['ga-disable-G-XXXX'] === true` the tag for that property sends
|
|
29
|
+
* nothing and writes nothing. Google's own documented opt-out mechanism, and
|
|
30
|
+
* the one that works on a tag which never received a consent-mode default
|
|
31
|
+
* (AGL-1608) — a case that still exists after AGL-1622, because a tag arriving
|
|
32
|
+
* through GTM or a host's own CMP is not one this adapter declared for.
|
|
33
|
+
*/ export const GA_DISABLE_FLAG_PREFIX = 'ga-disable-';
|
|
34
|
+
/**
|
|
35
|
+
* The consent-mode `default` declaration that goes in front of the tag
|
|
36
|
+
* (AGL-1622), for injection into the same inline block that creates
|
|
37
|
+
* `dataLayer` and calls `gtag('config', …)`.
|
|
38
|
+
*
|
|
39
|
+
* ## What this is, and — load-bearing — what it is NOT
|
|
40
|
+
*
|
|
41
|
+
* It is NOT a licence to load the tag earlier. It is emitted INSIDE the block
|
|
42
|
+
* that only renders when the AGL-1498 gate has already said yes, so it is
|
|
43
|
+
* reached only on a pageview where analytics is granted. On every gated
|
|
44
|
+
* pageview — an EU/UK/EEA visitor with no explicit accept, an unknown region, a
|
|
45
|
+
* declined or opted-out visitor, a GPC browser — nothing here renders, no
|
|
46
|
+
* request goes to `googletagmanager.com`, and this constant is never evaluated
|
|
47
|
+
* into the page. "The script never LOADS, rather than load-then-suppress" is a
|
|
48
|
+
* stated AGL-1498 property and it is intact everywhere the gate applies;
|
|
49
|
+
* `consent-mode-default.spec.tsx` fails if that ever stops being true.
|
|
50
|
+
*
|
|
51
|
+
* the decision, 2026-08-14: load-then-restrict is approved for the UNITED
|
|
52
|
+
* STATES, where the implied-consent posture already permits the load and the
|
|
53
|
+
* restriction signals act on a tag that is legitimately resident. EU and UK
|
|
54
|
+
* keep the prior-consent gate exactly as it is, because loading an analytics
|
|
55
|
+
* tag before consent is the specific thing prior-consent law prohibits. So the
|
|
56
|
+
* consent-mode mechanism is strictly ADDITIVE to the gate, never a replacement
|
|
57
|
+
* for it.
|
|
58
|
+
*
|
|
59
|
+
* What declaring it actually buys, given the tag is granted by the time it is
|
|
60
|
+
* read:
|
|
61
|
+
*
|
|
62
|
+
* - The advertising signals are denied FROM THE FIRST HIT. Without a default,
|
|
63
|
+
* a freshly loaded tag runs with `ad_storage` unrestricted and only becomes
|
|
64
|
+
* denied if the visitor happens to withdraw and change their mind again
|
|
65
|
+
* (AGL-1608's `update`). The load-time state now matches the tool's actual
|
|
66
|
+
* scope instead of being wider than it.
|
|
67
|
+
* - A later `update` is a transition from a declared state rather than a tag's
|
|
68
|
+
* first-ever consent signal, which is the shape GA4's consent-mode reporting
|
|
69
|
+
* and any third-party CMP alongside ours both expect.
|
|
70
|
+
*
|
|
71
|
+
* The value is a literal, built from `analyticsConsentSignals` — no
|
|
72
|
+
* interpolated input reaches it, which matters because it lands inside an
|
|
73
|
+
* inline script (the AGL-138 concern).
|
|
74
|
+
*/ export const GA_CONSENT_DEFAULT_SNIPPET = `gtag('consent', 'default', ${JSON.stringify(analyticsConsentSignals(true))});`;
|
|
75
|
+
/**
|
|
76
|
+
* The same declaration for a visitor who granted ADVERTISING too (AGL-1649).
|
|
77
|
+
*
|
|
78
|
+
* A second CONSTANT rather than a parameterised builder, for the reason the
|
|
79
|
+
* first one is a constant: it lands inside an inline script (the AGL-138
|
|
80
|
+
* concern), and a constant cannot interpolate anything. Both values are
|
|
81
|
+
* literals fixed at module load; the only thing the caller chooses is which
|
|
82
|
+
* of the two to emit.
|
|
83
|
+
*
|
|
84
|
+
* Reached only where the page's `advertising` answer is yes — host opted in,
|
|
85
|
+
* explicit accept, explicit yes to this category — which is evaluated
|
|
86
|
+
* client-side after hydration, like every other part of this gate. The ISR
|
|
87
|
+
* property is unaffected: the cached HTML contains neither snippet, because
|
|
88
|
+
* the whole block is inside the client-side `analyticsAllowed` condition.
|
|
89
|
+
*
|
|
90
|
+
* Declaring the grant as the DEFAULT rather than sending a later `update` is
|
|
91
|
+
* what keeps the tag's first hit correct. An update would arrive after
|
|
92
|
+
* `config`, so the session's first pageview — usually the entire session for
|
|
93
|
+
* a marketing visit — would carry the denied state the visitor did not
|
|
94
|
+
* choose.
|
|
95
|
+
*/ export const GA_CONSENT_DEFAULT_WITH_ADS_SNIPPET = `gtag('consent', 'default', ${JSON.stringify(consentModeSignals({
|
|
96
|
+
analytics: true,
|
|
97
|
+
advertising: true
|
|
98
|
+
}))});`;
|
|
99
|
+
/**
|
|
100
|
+
* Consent-mode URL passthrough for the platform's own tag (AGL-2548).
|
|
101
|
+
*
|
|
102
|
+
* The advertising signals in {@link GA_CONSENT_DEFAULT_SNIPPET} are denied, so
|
|
103
|
+
* on `aglyn.com` gtag writes no `_gcl_aw` cookie and the `gclid` an ad click
|
|
104
|
+
* arrives with lives only in that first URL. The conversions it should be
|
|
105
|
+
* credited for fire one hop later, on the console, where the posture grants
|
|
106
|
+
* advertising storage outside the prior-consent regions — and without this
|
|
107
|
+
* declaration the click id never makes that hop. The console reports a
|
|
108
|
+
* conversion with no click behind it, and Google Ads counts nothing.
|
|
109
|
+
*
|
|
110
|
+
* `url_passthrough` is Google's mechanism for exactly this state: while
|
|
111
|
+
* `ad_storage` is denied, gtag appends the click identifiers to links into the
|
|
112
|
+
* cross-domain-linked hosts as URL parameters instead of setting a cookie. The
|
|
113
|
+
* receiving tag reads them under its own consent state. Nothing is stored on
|
|
114
|
+
* the denying surface, so the marketing site's opt-in advertising posture is
|
|
115
|
+
* unchanged; the click id simply survives the link it was always meant to
|
|
116
|
+
* survive.
|
|
117
|
+
*
|
|
118
|
+
* `ads_data_redaction` is the companion Google documents alongside it: with
|
|
119
|
+
* `ad_storage` denied, the ad click identifiers are stripped from the hits this
|
|
120
|
+
* tag sends and its network requests go to a cookieless endpoint. Declared
|
|
121
|
+
* together so a denied visitor is measured with less, not merely differently.
|
|
122
|
+
*
|
|
123
|
+
* Both are `set` calls, so they must precede `gtag('config', …)` in the same
|
|
124
|
+
* inline block — a `set` applies to hits processed after it, and the session's
|
|
125
|
+
* first pageview is the one carrying the click.
|
|
126
|
+
*
|
|
127
|
+
* PLATFORM ID ONLY. A customer's tag is configured with their id and their
|
|
128
|
+
* consent posture; how their ad click ids travel is theirs to decide, and a
|
|
129
|
+
* passthrough declared on their behalf would rewrite links on their site.
|
|
130
|
+
*/ export const GA_CLICK_ID_PASSTHROUGH_SNIPPET = "gtag('set', 'url_passthrough', true);" + "gtag('set', 'ads_data_redaction', true);";
|
|
131
|
+
/** The marker `advertising-tags.ts` names in `sharesLibrary` for gtag.js. */ export const GTAG_LIBRARY = 'googletagmanager.com/gtag/js';
|
|
132
|
+
/** `dataLayer` and the canonical `gtag` shim, the start of every boot. */ const GTAG_SHIM = 'window.dataLayer=window.dataLayer||[];' + 'function gtag(){dataLayer.push(arguments);}';
|
|
133
|
+
/**
|
|
134
|
+
* The consent-mode default a boot opens with — none when the host runs its
|
|
135
|
+
* own consent solution (`consentRequired` false): their solution owns the
|
|
136
|
+
* default, and a second one racing it would overwrite their visitor's answer
|
|
137
|
+
* with ours.
|
|
138
|
+
*/ function consentDefault(options) {
|
|
139
|
+
if (!options.consentRequired) return '';
|
|
140
|
+
return options.advertising ? GA_CONSENT_DEFAULT_WITH_ADS_SNIPPET : GA_CONSENT_DEFAULT_SNIPPET;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The Google Analytics pair (AGL-138/661): the site's configured measurement
|
|
144
|
+
* id, booted and loaded.
|
|
145
|
+
*
|
|
146
|
+
* Order is load-bearing twice over. The inline block precedes the library, and
|
|
147
|
+
* inside it the consent `default` precedes `config`, so no hit is ever sent
|
|
148
|
+
* before the tag has been told what it may store. Google's canonical snippet
|
|
149
|
+
* shape.
|
|
150
|
+
*
|
|
151
|
+
* ## The platform's own property
|
|
152
|
+
*
|
|
153
|
+
* `aglyn.com` is a site on this platform pointed at the platform's measurement
|
|
154
|
+
* id, and three things are said to THAT property only, because a customer's
|
|
155
|
+
* site configures its own id and their property gets no opinion of ours:
|
|
156
|
+
*
|
|
157
|
+
* - THE INTERNAL-TRAFFIC STAMP (AGL-2064), between the shim and `gtag('js')`.
|
|
158
|
+
* It is a CONSTANT string, evaluated in the browser: these pages are
|
|
159
|
+
* ISR-cached, so a server-side branch on "is this us" would bake one
|
|
160
|
+
* browser's answer into the cache for everyone. It runs BEFORE `config`,
|
|
161
|
+
* because the hits that leak are the ones no call site writes —
|
|
162
|
+
* `session_start`, `first_visit`, `user_engagement`, the automatic
|
|
163
|
+
* `page_view` — and a `set` applies only to hits processed after it. Wrongly
|
|
164
|
+
* flagging a real visitor erases them from every report and a GA4 data
|
|
165
|
+
* filter is not retroactive, so the id equality is the guard that keeps the
|
|
166
|
+
* expensive direction unreachable. When the analytics escape hatch
|
|
167
|
+
* deliberately re-enables a non-production build, the stamp is
|
|
168
|
+
* UNCONDITIONAL (AGL-2067): a build that emits because someone asked it to is
|
|
169
|
+
* ours by definition.
|
|
170
|
+
* - THE CLICK-ID PASSTHROUGH (AGL-2548), also a `set` before `config`.
|
|
171
|
+
* - `content_group: 'marketing'` on `config` (AGL-1857): the one-click
|
|
172
|
+
* marketing/docs/console split in GA4 standard reports. The platform's id is
|
|
173
|
+
* the discriminator because same-property IS the definition of "this is our
|
|
174
|
+
* surface".
|
|
175
|
+
*/ function googleAnalyticsMount(measurementId, options) {
|
|
176
|
+
const own = measurementId === PLATFORM_GA_MEASUREMENT_ID;
|
|
177
|
+
return {
|
|
178
|
+
id: 'ga',
|
|
179
|
+
boot: GTAG_SHIM + consentDefault(options) + (own ? analyticsEnvironmentForcesInternal() ? INTERNAL_TRAFFIC_FORCED_SNIPPET : INTERNAL_TRAFFIC_GTAG_SNIPPET : '') + (own ? GA_CLICK_ID_PASSTHROUGH_SNIPPET : '') + "gtag('js', new Date());" + (own ? `gtag('config', '${measurementId}', {'content_group':'marketing'});` : `gtag('config', '${measurementId}');`),
|
|
180
|
+
src: `https://www.googletagmanager.com/gtag/js?id=${measurementId}`,
|
|
181
|
+
library: GTAG_LIBRARY
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The Google Tag Manager pair (AGL-2486), under the same gate as GA and never
|
|
186
|
+
* a looser one.
|
|
187
|
+
*
|
|
188
|
+
* A container is not a tag — it is a LOADER, and what it loads is decided in
|
|
189
|
+
* Google's UI by whoever owns it, not here. That is exactly why it cannot have
|
|
190
|
+
* a weaker gate than GA: analytics may run on implied consent outside the
|
|
191
|
+
* EU/EEA/UK while ADVERTISING is opt-in everywhere, and a container is the
|
|
192
|
+
* likeliest thing on a page to carry an advertising tag.
|
|
193
|
+
*
|
|
194
|
+
* CONSENT MODE V2 comes first, in the same script, before `gtm.js` is
|
|
195
|
+
* requested: defaults set after the container has loaded are defaults its
|
|
196
|
+
* tags have already run past. The advertising signals stay denied unless the
|
|
197
|
+
* visitor granted advertising, which is what makes a container holding ad
|
|
198
|
+
* tags safe to load at all.
|
|
199
|
+
*
|
|
200
|
+
* NO `<noscript>` IFRAME, deliberately, and it is the one piece of Google's
|
|
201
|
+
* standard snippet omitted. That iframe fires the container with no
|
|
202
|
+
* JavaScript — so no consent defaults, no gate, nothing to suppress it — and
|
|
203
|
+
* these pages are ISR-cached, so it would sit in shared HTML identical for
|
|
204
|
+
* every visitor and every region. A visitor with JavaScript off gets no
|
|
205
|
+
* container, which is the correct answer.
|
|
206
|
+
*/ function tagManagerMount(containerId, options) {
|
|
207
|
+
return {
|
|
208
|
+
id: 'gtm',
|
|
209
|
+
boot: GTAG_SHIM + consentDefault(options) + "dataLayer.push({'gtm.start':new Date().getTime(),event:'gtm.js'});",
|
|
210
|
+
src: `https://www.googletagmanager.com/gtm.js?id=${containerId}`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The measurement ids whose tag is actually RESIDENT in this page.
|
|
215
|
+
*
|
|
216
|
+
* Discovered from the page rather than taken from the host document on
|
|
217
|
+
* purpose: what has to be silenced is whatever gtag.js is currently loaded and
|
|
218
|
+
* configured for, which is not always what the host record configures — a tag
|
|
219
|
+
* can arrive through GTM, the console's is injected by its analytics SDK, and
|
|
220
|
+
* a stale `<script>` from earlier in the pageview is exactly the thing this
|
|
221
|
+
* function exists to find.
|
|
222
|
+
*
|
|
223
|
+
* Ids are format-checked before they are used, because each one becomes a
|
|
224
|
+
* `window` property name.
|
|
225
|
+
*/ export function residentGaMeasurementIds() {
|
|
226
|
+
if (typeof document === 'undefined') return [];
|
|
227
|
+
const ids = new Set();
|
|
228
|
+
try {
|
|
229
|
+
const loaded = document.querySelectorAll(`script[src*="${GTAG_LIBRARY}"]`);
|
|
230
|
+
for (const element of Array.from(loaded)){
|
|
231
|
+
var _element_src;
|
|
232
|
+
const src = String((_element_src = element.src) != null ? _element_src : '');
|
|
233
|
+
const match = /[?&]id=([^&]*)/.exec(src);
|
|
234
|
+
const id = match ? decodeURIComponent(match[1]) : '';
|
|
235
|
+
if (GA_MEASUREMENT_ID_PATTERN.test(id)) ids.add(id);
|
|
236
|
+
}
|
|
237
|
+
} catch (unused) {
|
|
238
|
+
// A hostile or absent DOM: the dataLayer pass below still applies.
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
var _window;
|
|
242
|
+
const layer = (_window = window) == null ? void 0 : _window.dataLayer;
|
|
243
|
+
for (const entry of Array.from(layer != null ? layer : [])){
|
|
244
|
+
var _args_;
|
|
245
|
+
// `arguments` objects, not arrays — gtag pushes its own call sites.
|
|
246
|
+
const args = Array.from(entry != null ? entry : []);
|
|
247
|
+
if (args[0] !== 'config') continue;
|
|
248
|
+
const id = String((_args_ = args[1]) != null ? _args_ : '');
|
|
249
|
+
if (GA_MEASUREMENT_ID_PATTERN.test(id)) ids.add(id);
|
|
250
|
+
}
|
|
251
|
+
} catch (unused) {
|
|
252
|
+
// No dataLayer, or one that is not iterable: the script pass stands.
|
|
253
|
+
}
|
|
254
|
+
return [
|
|
255
|
+
...ids
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Make every resident Google tag agree with the visitor's answer, and return
|
|
260
|
+
* the measurement ids it acted on.
|
|
261
|
+
*
|
|
262
|
+
* ## Why deleting the cookies is not enough (AGL-1608)
|
|
263
|
+
*
|
|
264
|
+
* The AGL-1498 gate stops the script from LOADING, which is the right
|
|
265
|
+
* enforcement for a fresh pageview and the wrong tool for a visitor who
|
|
266
|
+
* withdraws mid-pageview: `gtag.js` has already executed, and React unmounting
|
|
267
|
+
* the `<script>` element does not unload it. `window.gtag` stays live, GA4
|
|
268
|
+
* enhanced measurement fires on its own (scroll depth, outbound click, file
|
|
269
|
+
* download), and the tag re-writes `_ga_<id>` AFTER the platform's cookie
|
|
270
|
+
* sweep has deleted it. Reproduced on aglyn.com: an opt-out, a hand-run sweep
|
|
271
|
+
* to zero cookies, then one scroll to the footer brought `_ga_YW5PG16YTM`
|
|
272
|
+
* back.
|
|
273
|
+
*
|
|
274
|
+
* Two signals, because they fail differently and neither is guaranteed:
|
|
275
|
+
* the `ga-disable-<id>` window flag silences a tag that never received a
|
|
276
|
+
* consent-mode default, and the `consent`/`update` call reaches a tag that
|
|
277
|
+
* arrived through GTM with its own id this function never saw.
|
|
278
|
+
*
|
|
279
|
+
* ## What this deliberately does NOT do
|
|
280
|
+
*
|
|
281
|
+
* It never touches the gate. Both signals act only on a tag that is already
|
|
282
|
+
* resident — which, by construction, only exists after a grant. The
|
|
283
|
+
* consent-mode DEFAULT ({@link GA_CONSENT_DEFAULT_SNIPPET}) is declared inside
|
|
284
|
+
* the gated block and therefore only on a pageview the gate already allowed.
|
|
285
|
+
*
|
|
286
|
+
* Symmetric on purpose: a visitor who opts out and changes their mind in the
|
|
287
|
+
* same pageview would otherwise stay silently unmeasured until they navigated,
|
|
288
|
+
* because the re-rendered `<Script>` cannot re-execute an already-loaded tag.
|
|
289
|
+
* The re-grant restores exactly what `grants` says, and core clamps
|
|
290
|
+
* advertising to analytics before it arrives, so a withdrawal can never
|
|
291
|
+
* restore advertising alone.
|
|
292
|
+
*/ export function applyGoogleTagConsent(grants) {
|
|
293
|
+
if (typeof window === 'undefined') return [];
|
|
294
|
+
const granted = grants.analytics === true;
|
|
295
|
+
const ids = residentGaMeasurementIds();
|
|
296
|
+
const scope = window;
|
|
297
|
+
for (const id of ids){
|
|
298
|
+
scope[`${GA_DISABLE_FLAG_PREFIX}${id}`] = !granted;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const send = residentGtag();
|
|
302
|
+
if (send) {
|
|
303
|
+
// The same payload builder the load-time `default` is made from
|
|
304
|
+
// (AGL-1622), so the two declarations cannot drift.
|
|
305
|
+
send('consent', 'update', consentModeSignals({
|
|
306
|
+
analytics: granted,
|
|
307
|
+
advertising: grants.advertising === true
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
} catch (unused) {
|
|
311
|
+
// A tag that throws on its own consent call is one we cannot silence;
|
|
312
|
+
// the flag above and the cookie sweep still stand.
|
|
313
|
+
}
|
|
314
|
+
return ids;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Hand one event to the resident tag.
|
|
318
|
+
*
|
|
319
|
+
* ## `measurementOnly`: the measurement properties, not the ads account (AGL-2710)
|
|
320
|
+
*
|
|
321
|
+
* A gtag event with no `send_to` goes to every destination the loader has
|
|
322
|
+
* configured, and on a surface that also runs Google Ads that includes the
|
|
323
|
+
* ads account. Core Web Vitals have nothing to do with advertising: measured
|
|
324
|
+
* on `aglyn.com`, one such event attempted seven requests to
|
|
325
|
+
* `googleads.g.doubleclick.net` and `google.com/{pagead,rmkt,ccm}` — per
|
|
326
|
+
* metric, per pageview, four metrics deep — and Google Ads has no report that
|
|
327
|
+
* reads any of them. Naming the GA4 ids drops all seven and leaves the GA hit
|
|
328
|
+
* exactly as it was.
|
|
329
|
+
*
|
|
330
|
+
* The ids are read at DELIVERY time, from the page: the tag routinely arrives
|
|
331
|
+
* after the first metric, and what has to be addressed is whatever gtag is
|
|
332
|
+
* configured for now — which on a customer site is their own property and not
|
|
333
|
+
* ours.
|
|
334
|
+
*
|
|
335
|
+
* No ids found means no `send_to` and the event goes wherever it went before.
|
|
336
|
+
* That is the fail-open side on purpose: a tag that arrived through GTM with
|
|
337
|
+
* an id this reader never saw would otherwise be handed an empty destination
|
|
338
|
+
* list, and losing the measurement is a worse outcome than an ads account
|
|
339
|
+
* receiving a metric it ignores.
|
|
340
|
+
*/ function sendGoogleTagEvent(name, params, options) {
|
|
341
|
+
const gtag = residentGtag();
|
|
342
|
+
if (!gtag) return;
|
|
343
|
+
try {
|
|
344
|
+
if (!(options == null ? void 0 : options.measurementOnly)) {
|
|
345
|
+
gtag('event', name, params);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const destinations = residentGaMeasurementIds();
|
|
349
|
+
gtag('event', name, destinations.length ? _extends({}, params, {
|
|
350
|
+
send_to: destinations
|
|
351
|
+
}) : params);
|
|
352
|
+
} catch (unused) {
|
|
353
|
+
// Analytics never breaks the page.
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/** The adapter core registers from each app's generated manifest. */ export const analyticsProvider = {
|
|
357
|
+
mounts (host, options) {
|
|
358
|
+
const mounts = [];
|
|
359
|
+
const measurementId = resolveGaMeasurementId(host);
|
|
360
|
+
if (measurementId) mounts.push(googleAnalyticsMount(measurementId, options));
|
|
361
|
+
const containerId = resolveGtmContainerId(host);
|
|
362
|
+
if (containerId) mounts.push(tagManagerMount(containerId, options));
|
|
363
|
+
return mounts;
|
|
364
|
+
},
|
|
365
|
+
applyConsent: applyGoogleTagConsent,
|
|
366
|
+
resident: ()=>residentGtag() !== null,
|
|
367
|
+
sendEvent: sendGoogleTagEvent
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
//# sourceMappingURL=analytics-provider.js.map
|