What this shows: Two complete diagrams — a Flowchart and a Sequence Diagram — covering the full path every analytics event takes from a user action to Adobe reporting.
Scope: Full monorepo — NVC, NaBuy, Shop FBC, IMG, FSC, NaShop
Audience: Engineers, tech leads, platform architects
Last updated: 2026-06-12
What this shows: The complete path every analytics event takes across all monorepo domains — from a user action to Adobe data collection.
A real user does something on the page.
This could be clicking a button, loading a page, applying a filter, adding to cart, or any trackable interaction.
This is the starting point of every analytics event.
The developer has wired an analytics call into the component code.
When the user action happens, the component fires an analytics call.
Problem: Not all components use the same method to fire. Some use shared hooks, some call satellite directly.
This is the first big problem point.
We do not have one analytics system. We have five different entry points across the monorepo.
Depending on which app or domain the component belongs to, the event takes a completely different path.
This is why behavior is inconsistent. Same click, different apps, different outcomes.
Even within the NVC shared hook, there is another decision point.
If the current tenant (brand + commodity + country + language) is in the registry-enabled list, it goes one way.
If not, it falls back to the old path.
Today only ford/plans/ZA/en is in the enabled list. Everything else takes the legacy path.
This means the same user action produces different payload structures in different markets.
The FSC (Ford Smart Checkout / Finance Calculator) package manages its own analytics completely.
It has its own initialization, its own validation functions, and calls _satellite.track directly.
It does not go through @shared/analytics at all.
Problem: FSC and NVC can both write to window.digitaldata with no coordination between them.
The NaBuy Cart app has its own analytics provider, its own constants, and its own JavaScript files in the public folder.
It calls window._satellite.track directly from those files.
Problem: Same global object, different writer, no shared contract.
The Shop FBC app uses a Metrics class with static methods.
This class manages its own digital data lifecycle and satellite calls.
Problem: Another independent writer to the same global state.
IMG apps (vehicle configurator, order management) import a DIGITALDATA constant object and build their own payload structures.
They have their own page event functions and filter analytics helpers.
Problem: Fourth independent path. No shared validation or contract.
When registry is enabled, the hook calls into the registry builder.
The builder reads analyticsRegistryConfig for the current tenant and assembles the payload from config definitions.
This is the newer, more structured approach.
This is what we want for all tenants — but today it only works for one.
When registry is not enabled, the hook falls into the legacy builder.
These are large functions with many switch cases and inline payload construction.
Each variant (PDP, cart, checkout, search) has its own builder logic.
Problem: Hard to change safely. Adding one variant can break another. ESLint complexity lint is suppressed here.
Both registry and legacy paths funnel into the same analytics.send function.
This function decides whether to fire immediately or queue the event.
This is the merge point where both paths converge.
This is the most dangerous point in the whole system.window.digitaldata is a shared global object that every subsystem writes to.
The merge function can either merge fields onto the existing object or completely replace it.
If replace mode is used at the wrong time, fields set by another team disappear silently.
Example: Team A sets page.pageName. Team B fires onclick with replace mode. page.pageName is gone. Adobe mapping fails.
FSC, NaBuy, Shop FBC, and IMG apps all bypass the shared analytics.send entirely.
They call window._satellite.track directly after writing to window.digitaldata themselves.
This means four parallel writers with no coordination, no validation, and no contract enforcement.
Adobe's satellite tracker is loaded asynchronously by a third-party script tag.
When an event fires before the script has loaded, window._satellite does not exist yet.
The system checks if it is available before trying to fire.
If satellite is not ready, the event is held in an in-memory queue.
When the satellite script finishes loading, it fires a custom window event called ANALYTICS_LOADED.
The queue listens for that event and then flushes all held events.
Problem: Queue deduplication uses deep equality. If the same event fires twice with slightly different payload fields, both go through.
The satellite tracker receives the event type string and reads the current state of window.digitaldata.
It sends that data to the Adobe network endpoint.
Critical: Satellite reads from the global object at fire time. Whatever is in window.digitaldata at that exact moment is what gets sent. If the object was corrupted by another writer, bad data goes to Adobe.
The data leaves the browser and goes to Adobe's data collection servers.
Once this POST fires, the data is in Adobe's pipeline.
There is no recall or correction mechanism after this point.
Adobe receives the payload and processes it into reporting dimensions and metrics.
If fields are missing, wrong, or named incorrectly, the data either lands in the wrong dimension or is dropped.
There is no error returned to the browser. Bad data lands silently.
Our Playwright E2E tests intercept the POST request before it reaches Adobe.
They run Zod schema validation on the payload and check for duplicate fires.
Problem: This is the first hard validation gate in the entire pipeline. By this point the event has already been built and sent. Schema errors are caught very late.
Adobe processes the data and feeds it into reporting dashboards and data engineering pipelines.
If the payload was inconsistent or wrong, the reports show inconsistent numbers.
Analytics teams may not notice until a weekly review or quarterly audit.
| Block | Problem |
|---|---|
| C — Which subsystem | Five independent paths with no unified contract |
| D — isRegistryEnabled | Market behavior divergence from a single hardcoded gate |
| E F G H — Direct paths | Four teams writing to same global object with no coordination |
| M O — mergeDigitalData | Shared mutable global state is the integration contract |
| N — Direct satellite | No validation, no contract, no shared lifecycle |
| Q — satellite.track | Reads corrupted global state if another writer interfered |
| V — E2E validation | First real validation gate is at the end of the pipeline, too late |
What this shows: The exact step-by-step message flow between every system component — from the developer writing code to data landing in Adobe.
Scope: NVC shared analytics path (covers search, pdp, home, cart, checkout)
A developer is building a feature — say a Clear Filters button or an Add to Cart button.
They add an analytics call in the component code.
They choose which event type and variant to use.
Problem: Every developer makes this choice independently. There is no mandatory contract to follow.
Before any event is fired, the page renders two separate analytics providers stacked on top of each other.NvcAnalyticsProvider holds: brand, commodity, country, language.ShopAnalyticsProvider holds: CommodityConfig with site, eventDataPrefix, rulePrefix, pageVariant.
Every analytics hook reads from both of these.
Problem: Two providers serving different parts of the same payload creates two sources of truth.
The hook is invoked with an event type string like websdk-onclick and a variant string like clear-filters.
The hook reads both providers to understand the current tenant context.
This is where the analytics path diverges based on market.
The hook calls isRegistryEnabled(nvcAnalyticsCtx).
This checks a hardcoded object in the registry resolver source code.
If the current brand/commodity/country/lang combination is in the list, it goes to the registry builder.
If not, it falls to the legacy builder.
Today only ford/plans/ZA/en returns true. All other markets go legacy.
When registry is enabled, the hook calls the registry action builder.
The builder looks up the current tenant in analyticsRegistryConfig — a large config object.
It finds the matching click configuration for the current app namespace and variant.
It resolves field values, applies page data, enriches with user/vehicle data.
It returns a typed action object with the correct satellite track name and full payload.
This is the right approach. Config-driven, deterministic, tenant-specific.
When registry is not enabled, the hook falls into the legacy builder.
The legacy builder has a large switch statement on event type.
Inside each case there are further conditionals on variant.
Functions like getGlobalLoad, getGlobalPdp, and getCartCheckoutPayload build the payload manually.
Problem: This is 927 lines of orchestration code. Complexity lint is suppressed. Any change can accidentally break another variant.
Both paths send their built action object to the shared analytics.send function.analytics.send checks if the library is initialized. If not, it calls analytics.init() first.
It then calls mergeDigitalData to apply the payload to the global object.
It then calls _satellite.track to fire the event.
This is the last shared point before the event is committed.
This is the highest-risk step in the entire flow.window.digitaldata is a global object shared by every app, every team, every subsystem.mergeDigitalData either merges new fields onto the existing object or completely replaces it.
The mode depends on a replaceDigitalData flag or shouldUseCurrentDataLayer setting in the config.
If replace mode runs when merge was expected, fields from a previous event disappear. Adobe receives an incomplete payload silently.
Satellite is Adobe's tag management system loaded by a third-party script.
When _satellite.track(eventType) is called, satellite reads the current state of window.digitaldata and sends it.
Critical: Satellite reads the global object at the exact moment of the call. If that object was corrupted in Step 7, corrupted data goes to Adobe. There is no recall.
If the satellite script has not finished loading when the event fires, the track call would fail.
Instead, the event is added to an in-memory queue.
The queue waits for a custom window event called ANALYTICS_LOADED.
Once that fires, the queue flushes all held events.
Problem: Queue deduplication checks deep equality of the action object. If two near-identical events differ by one field, both go through and Adobe gets duplicates.
Satellite sends an HTTP POST to Adobe's data collection endpoint.
The body contains the full serialized payload from window.digitaldata.
Once this fires, the data is in Adobe's pipeline.
There is no way to correct or recall data after this point.
Our Playwright E2E tests intercept this POST before it reaches Adobe in test environments.
They extract the event data, run it through Zod schema validation, and check for duplicate fire.
If validation fails, the test fails with field-level error detail.
Problem: This is the first hard contract validation gate in the entire pipeline. A schema error would pass unit tests, pass code review, pass staging, and only be caught here — at end-to-end test stage. For production bugs not covered by E2E tests, the error reaches Adobe undetected.
Adobe processes the payload and maps fields to reporting dimensions and metrics.
Downstream data engineering pipelines consume from Adobe.
Business dashboards show conversion, click, and funnel data from this.
If the payload was inconsistent or had wrong field names, numbers will be wrong. The reporting team may not notice until a weekly review or a campaign post-analysis.
| Step | What We See | Why It Is a Problem |
|---|---|---|
| Step 2 | Two providers stacked | Two sources of truth for the same payload |
| Step 4 | Registry gate | Market behavior is determined by a hardcoded list in source code |
| Step 5b | Legacy builder 927 lines | High change risk, suppressed lint, any edit can break other variants |
| Step 7 | mergeDigitalData | Shared global mutation is the integration contract between all teams |
| Step 8 | _satellite.track reads global | Corrupted state goes to Adobe with no validation gate before send |
| Step 9 | Queue deep equality dedup | Near-duplicate events can still double-fire |
| Step 11 | E2E is first schema gate | Contract errors are not caught until end-to-end test or production |
| Step 12 | Silent Adobe ingest | Wrong data lands in reports with no real-time alert |
| Today | Target |
|---|---|
| Developer picks event type and builds payload | Developer emits typed intent by contract key |
| Two providers with separate configs | One unified tenant context |
| Registry gate in source code | Rollout policy in configuration |
| Legacy builder with 927-line switch | Contract-driven builder from registry |
| Shared mutable global state | Managed data layer with strict merge-only lifecycle |
| No validation before send | Zod schema validation blocks invalid payloads before send |
| E2E is first validation gate | CI schema lint is first gate, E2E confirms end-to-end |
| Silent Adobe ingest for bad data | Observability alerts on schema violations and duplicates |
Document maintained by: NVC Platform Architecture Team
Last updated: 2026-06-12