# Failure modes: what goes wrong in production and how to detect it

Twenty-three patterns seen when plans meet production data, grouped by kind. Each names the cause, how it shows in the inventory, the rule that catches it, and one check. Replace `your-project.analytics_XXXXXXXXX` and the dates in the SQL.

## Contents

- Duplicates
- Names
- Types
- Completeness
- Lifecycle
- Privacy and tool illusions
- Generated names, missing IDs and day boundaries

## Duplicates

### 1. Two producers, one name

- Cause: a browser tag or SDK plus a server or CDP route, or a page-based tag beside a dataLayer event.
- Shows as: a ratio above 1.0 against the system of record (D12); required properties present on only the planned producer's share of rows (D07); two sources for one name (D06).
- Check: the share of rows carrying a property only the planned producer sends is usually reference count divided by observed count. For GA4 purchases:

```sql
SELECT ecommerce.transaction_id, COUNT(*) AS row_count
FROM `your-project.analytics_XXXXXXXXX.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261003' AND '20261030' AND event_name = 'purchase'
GROUP BY 1 HAVING COUNT(*) > 1
ORDER BY row_count DESC LIMIT 100;
```

### 2. Autocapture counted as plan coverage

- Cause: a click or autocaptured interaction used as an outcome event.
- Shows as: outcome counts above the system of record (D12). PostHog's own guidance warns that relying on autocapture can miss an explicit signup event.
- Check: compare the outcome with the server-confirmed count for the same window and exclusions.

## Names

### 3. One action, several spellings

- Cause: platform teams naming on their own; a connector's lowercase and underscore settings; warehouse slugging.
- Shows as: D03 with rows on both spellings.
- Check: Segment warehouse Query 1b in [../assets/segment-warehouse-inventory.sql](../assets/segment-warehouse-inventory.sql) lists tables whose `event` slug holds more than one `event_text`.

### 4. Dynamic names and keys

- Cause: IDs, dates or variant names inside event names or property keys.
- Shows as: D04 pattern groups; Amplitude event type counts near 2,000; a Mixpanel Lexicon export near its 5,000-event file limit.
- Check: PostHog Query 2 or the profiler output, sorted by name, shows the runs of near-identical names.

### 5. UI fixes that leave raw data split

- Cause: Mixpanel merges and display names, Amplitude display names, and hidden events, which are still ingested and billed.
- Shows as: a clean-looking report while the raw export still holds both names (D03).
- Check: audit raw names from exports, never display names. Mixpanel's Data Standards checks the display name when one is set, so it can look compliant while raw names are not.

## Types

### 6. Numbers sent as text

- Cause: quoted tag manager variables such as `'{{DLV - value}}'`, string building in code, form field values.
- Shows as: D10 "numbers sent as text"; GA4 `string_value` on a numeric parameter; Lexicon type String on a money property.
- Check, GA4:

```sql
SELECT
  COUNTIF(ep.value.string_value IS NOT NULL) AS as_text,
  COUNTIF(ep.value.int_value IS NOT NULL OR ep.value.double_value IS NOT NULL) AS as_number
FROM `your-project.analytics_XXXXXXXXX.events_*`, UNNEST(event_params) AS ep
WHERE _TABLE_SUFFIX BETWEEN '20261003' AND '20261030'
  AND event_name = 'purchase' AND ep.key = 'value';
```

### 7. Type frozen by the first value

- Cause: Segment warehouse columns typed when created; PostHog detected property types.
- Shows as: a column-level D10 finding (no share, since there are no counts).
- Check: Segment Query 2 (`information_schema`) or PostHog Data management. Changing a Segment column type needs Segment support, and backfill needs the tables rebuilt.

### 8. IDs sent as numbers

- Cause: database IDs serialized as JSON numbers.
- Shows as: D10 "IDs sent as numbers"; leading zeros lost; D14 numeric IDs above 2^53, which lose digits permanently in Mixpanel.
- Check: the profiler's `--identity-keys` classes, and the integer rows on any `_id` property.

### 9. Placeholder strings

- Cause: unset variables rendered as "undefined", "null", "NaN" or "[object Object]".
- Shows as: `sentinel_hits`, counted as missing in D07.
- Check: PostHog Query 1 counts them per property; the profiler counts them for every property.

## Completeness

### 10. Enforcement that drops silently

- Cause: Amplitude's reject setting stores the event without the unplanned property; Segment's Omit Properties applies only to cloud-mode and storage destinations; RudderStack drop settings.
- Shows as: completeness lower at the destination than at the source or in the warehouse (D07 per tool), while device-mode destinations still receive what Segment shows as omitted.
- Check: compare the same property's share present in the warehouse (Segment Query 2b) and in each tool.

### 11. GA4 silent ceilings

- Cause: more parameters than the limit (25 per event on standard properties, 100 on Analytics 360), names over 40 characters, values over 100 characters.
- Shows as: D14; a planned parameter missing only on rows that sit at the limit; Q09, and D01 for an over-long or reserved planned name.
- Check: Part A `max_params` in the GA4 query, which counts custom parameters only, and one DebugView capture to see which parameters were dropped.

### 12. Reserved columns discarded

- Cause: a property named like a Segment warehouse reserved column, for example `user_id`.
- Shows as: a planned property that never appears as a column although the payload carries it.
- Check: rename the property in the plan and the emitter; the warehouse discards it by design.

### 13. Property scope confusion

- Cause: a planned event property implemented only as a user or profile property.
- Shows as: D07 at 0% on the event while the value exists on the person.
- Check: GA4 Part E (`user_properties` on planned events), Amplitude `user_properties` in the export, Mixpanel profiles.

## Lifecycle

### 14. Plan says verified, tool says nothing

- Cause: the emitter sits behind a disabled flag, was removed in a refactor, was renamed by a GA4 modification rule, or is filtered by a CDP destination filter.
- Shows as: D01 stopped, with a `last_seen` date near a release: before the window, or inside it when the release is less than 28 days old, for the whole event or one platform (an iOS release while web keeps sending).
- Check: daily counts around `last_seen`, then [emitter-search.md](emitter-search.md), the release log for that date, and the GA4 event rules list.

### 15. Retired events from old app versions

- Cause: mobile installs that never update.
- Shows as: D05 with rows concentrated in old versions.
- Check: profile with `--source-key version_name` (Amplitude) or the app version field, then apply the version tail rule.

### 16. Enum values mapped silently

- Cause: a new provider or plan mapped onto an old value, or a raw new value shipped without a plan change.
- Shows as: D11; growth of "other" or "unknown".
- Check: compare value shares month on month; a new value at a stable total often replaced an old one.

### 17. Copy drift blocks good data

- Cause: the canonical plan changed but the Protocols, Amplitude Data, Avo or RudderStack copy did not.
- Shows as: blocked counts in Segment's Source Schema, or unexpected flags in Amplitude, on events the canonical plan lists.
- Check: compare the copy with the canonical tabs; fix the copy first, because blocked data cannot be recovered.

## Privacy and tool illusions

### 18. Personal data through free text and URLs

- Cause: search terms, error messages, `page_location` query strings, identify traits sent as event properties.
- Shows as: D13 property names and pattern counts. Values are never printed.
- Check: GA4 Part B `pii_hits` on `page_location`; the profiler's counts on every string property.

### 19. Short vendor windows read as clean

- Cause: Avo issues cover 24 hours, Amplitude validation errors 24 hours, Amplitude Observe 14 days, Segment violations 7 days, Mixpanel dropdowns 30 days for events and 28 for properties.
- Shows as: "no issues" in a view whose window is shorter than the audit.
- Check: record the window behind every "no issues" statement, and use exports or queries for 28 and 90 days.

### 20. Test traffic in production

- Cause: QA events, staging hosts or internal accounts in the production project.
- Shows as: D02 with the test label; a high `is_internal` share; non-production hosts in `page_location`.
- Check, GA4:

```sql
SELECT NET.HOST((SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location')) AS host, COUNT(*) AS row_count
FROM `your-project.analytics_XXXXXXXXX.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261003' AND '20261030'
GROUP BY host ORDER BY row_count DESC LIMIT 50;
```

## Generated names, missing IDs and day boundaries

### 21. Names the SDK or the CDP generates

- Cause: mobile SDK lifecycle tracking (Application Installed, Opened, Updated, Backgrounded) in the Segment, RudderStack and PostHog libraries, which then reaches every tool the CDP feeds; Segment destination page settings, which turn page and screen calls into "Loaded a Page" or into one "Viewed <name> Page" event per page in Amplitude and Mixpanel; web SDKs that add UTM parameters and click IDs to every event.
- Shows as: high-volume names in the top 20 that no code calls by that name; dozens of "Viewed ... Page" event types; utm_ properties on every event.
- Rule: the lifecycle, "Loaded a Page" and UTM entries in the vendor lists keep them out of D02 and D09; per-page names group into one D04 finding.
- Check: the SDK's lifecycle option in the app code and the destination's page settings in the CDP. Fix per-page names with Track All Pages, which sends one "Loaded a Page" event with the page name as a property, after moving the reports that read the old names.

### 22. IDs missing on events that should carry them

- Cause: events sent before identify, server events built without the account context, B2B events that never got workspace_id, or a tool that removes IDs it does not accept (Amplitude removes IDs under 5 characters and rejects its invalid list).
- Shows as: D14 null or placeholder shares per event where the plan names the ID with no null rule; funnels and group analytics that split at login.
- Check: the profiler with `--identity-keys user_id,event_properties.account_id`, GA4 Part F or Segment Query 4, then identity-merge-qa for split or wrongly merged users.

### 23. Day boundaries and late data move a ratio

- Cause: GA4 `event_date` follows the property time zone while the system of record counts UTC days; GA4 keeps filling daily tables for up to 3 days after the table date; mobile events arrive late.
- Shows as: a boundary-day offset that turns 1.000 into 1.003 and reads as double counting, or a server route at 0.98 because the last days of the window are still filling.
- Rule: D12 rates 0.99 to 1.01 WARN when the time zones are not confirmed to match, and caps a server-route shortfall at WARN when the window ends less than 4 days before the as-of date.
- Check, GA4 counts in UTC days:

```sql
SELECT event_name, COUNT(*) AS row_count
FROM `your-project.analytics_XXXXXXXXX.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20261002' AND '20261031'
  AND event_timestamp >= UNIX_MICROS(TIMESTAMP '2026-10-03 00:00:00+00')
  AND event_timestamp < UNIX_MICROS(TIMESTAMP '2026-10-31 00:00:00+00')
  AND event_name = 'sign_up'
GROUP BY event_name;
```
