Third-Party Mode (Planned)
In third-party mode, ONECMS generates the same typed, normalized GA-compatible payload but does not own the delivery pipeline. An external library provides its own script tags and handles sending events to GA4.
When to Use This Mode
This mode applies when ONECMS integrates a third-party package that:
- Ships its own GA4 / GTM script tags
- Has its own
gtagordataLayerinitialization - Expects a GA-compatible event payload from the host application
In this scenario, injecting a second gtag.js would cause double-counting, tag conflicts, or measurement ID collisions. The CMS should not load its own script — only generate the payload.
What Changes
| Concern | Owned Pipeline (Mode 1) | Third-Party Mode (Mode 2) |
|---|---|---|
| Script injection | GoogleAnalytics component loads gtag.js |
External lib provides its own tags |
window.gtag |
Initialized by our script | Initialized by external lib (or not used) |
| Payload generation | GA.trackAnalytics() + normalizeAnalyticsPayload() |
Same |
| Event types | AllAnalyticsEnvelopes |
Same |
| Delivery | window.gtag('event', ...) |
External SDK's send method |
| Consent | GA.updateGtagConsent() |
External lib's consent API |
What Does Not Change
The entire event generation and normalization layer remains identical:
- Typed envelopes (
AllAnalyticsEnvelopes) normalizeAnalyticsPayload()adapter- Schema versioning
- Production-only gating
- Domain helper functions
This is by design — the envelope is the contract, delivery is pluggable.
Expected Architecture

Implementation Guidance
When a third-party analytics package is installed:
1. Disable the owned script
Remove or conditionally skip the GoogleAnalytics component in ClientWrapper. This can be driven by a config flag or feature flag:
// Example — config-driven
{!Config.GA.useThirdPartyMode && (
<GoogleAnalytics measurementId={Config.GA.measurementId} />
)}
2. Create a delivery adapter
Add a new delivery adapter alongside the existing track() method in GA.ts:
private static trackViaThirdParty(event: string, payload: Record<string, unknown>) {
// Call the external SDK's send/track method
ExternalAnalyticsSDK.send(event, payload);
}
3. Route based on mode
trackAnalytics() dispatches to the correct delivery method:
static trackAnalytics(envelope: AllAnalyticsEnvelopes) {
const eventName = `${envelope.category}_${envelope.event}`.toLowerCase();
const finalPayload = normalizeAnalyticsPayload({
...envelope,
schema_version: Config.GA.schema_version,
});
if (envelope.env !== 'production') {
console.debug('[GA - Skipped in non-prod]', eventName, finalPayload);
return;
}
if (Config.GA.useThirdPartyMode) {
this.trackViaThirdParty(eventName, finalPayload);
} else {
this.track(eventName, finalPayload);
}
}
4. Adapt consent
If the external library has its own consent API, updateGtagConsent() should delegate to it instead of calling window.gtag('consent', ...).
Constraints
- The external library must accept a flat key-value payload (GA4's event parameter model)
- Event names must remain in
category_eventformat to preserve dashboard compatibility schema_versionmust still travel with payloads- Production gating must still be enforced — the external SDK should not receive non-production events
- No domain code should need to change — the switch is entirely at the transport layer
Open Questions
These will be resolved when the specific third-party package is selected:
- What is the SDK's send/track API signature?
- Does the SDK initialize its own
dataLayer, or does it use a different mechanism? - Does the SDK handle consent natively, or does it need consent state from the CMS?
- Should both modes be active simultaneously (e.g., owned pipeline for some events, third-party for others)?