# Event contract template. One file per canonical event, proposed for:
#   martech-context/tracking-plan/<event_name>.yaml
# Draft it in the conversation or in a scratch file outside martech-context/.
# Nothing is written to martech-context/ until the user approves the file list.
# This is a planning document, not SDK or vendor configuration.
# Field guide: references/contract-fields.md
# Delete the comments once the contract is approved.

event: object_action_past_tense        # canonical name, lowercase snake_case, per naming-rules.md
display_name: Object Action Past Tense  # label for Lexicon, Amplitude Data and reports
version: 1                             # integer; bump on breaking changes only (references/change-control.md). A compatible change updates the enforcement plan for the current version before release, with no bump
status: proposed                       # proposed | approved | implemented | verified | deprecated | retired
risk_tier: 2                           # 1 critical, 2 standard, 3 minor (references/change-control.md)
journeys: [journey-slug]               # journey files in martech-context/tracking-plan/ that use this event

# Why the event exists
business_question: >
  One sentence naming the decision this evidence supports.
consumers:                             # every event needs at least one
  - type: metric                       # metric | investigation | experiment | workflow
    ref: metric_id_v1                  # id from martech-context/metrics.md, report name or workflow name
grain: One <thing> that <reached a state>   # what exactly one event represents

# When it fires
trigger: >
  The state change that makes the fact true, observed by the source of truth.
must_not_fire:                         # at least two, in order; item n gets negative case T2.n below
  - UI intent step (button click, modal opened, form submitted but not yet validated)
  - Validation or operation failed
  - Page refresh, re-render, back navigation or success-page revisit
  - Delivery retry of an occurrence that was already sent
internal_and_test_traffic: flag        # flag (is_internal property) | suppress | send_to_test_project

source_of_truth:
  system: backend                      # backend | browser | mobile_app | billing_webhook | crm
  component: Name of the service, page or job that emits
  reason: Why this component is the one that can establish the fact
  other_emitters: none                 # or each companion emitter, its own event name, and the reconciliation rule

# Identity, all captured at event time
identifiers:
  event_id: Plain UUID generated once at the source when the fact occurs; persisted with any queued copy and reused on every retry; companion emissions carry it as a property. A business key is hashed into a UUIDv5, never prefixed or concatenated
  message_id: UUIDv5(plan namespace, event_id + ":" + emitted event name + ":" + emitter). One per emission, the same on that emission's retries, and the value every destination dedupe field receives. Never event_id itself
  anonymous_id: Device or browser id from the collection SDK; required before login, optional after; or not applicable with a reason
  user_id: Stable internal user id of the actor; never an email; null allowed only when initiated_by is system
  account_id: Commercial account at event time, taken on the server from the authenticated session or the object's record; never from ids the client sends in analytics calls, never reconstructed from current membership
  workspace_id: Workspace or project at event time, if the product has one
  object_ids:                          # business objects the event is about
    example_object_id: Internal id of the object

# Event properties (facts about this occurrence). Shared properties live in data-dictionary.md and are referenced by name.
# Every property states unknown_value_policy and null_means.
properties:
  example_enum_property:
    type: string
    required: true
    allowed_values: [value_a, value_b]
    unknown_value_policy: reject       # reject | send_unknown_and_alert | allow_and_alert; never map silently to an existing value
    null_means: never null; a null is a defect and is rejected
    description: What the value means at event time
  example_amount:
    type: integer
    required: false
    format: integer, 0 or more
    unknown_value_policy: reject values outside the format   # for non-enumerated properties this is the format-violation policy
    unit: minor currency units (cents)
    currency_property: currency        # ISO 4217 code in its own property
    definition: gross order value before refunds, excluding tax and shipping
    null_means: not applicable (free plan), not unknown   # not applicable | not yet known | withheld
  example_ref_property:
    ref: data-dictionary.md#property_name   # the shared definition holds type, policy and null_means

user_traits_updated: []                # traits this event's identify call may update; traits are partial, keep them optional
account_traits_updated: []

# What must never be sent
pii_exclusions:
  - Email addresses, names, phone numbers, postal addresses
  - Free-text fields (search terms, notes, raw error messages or provider error bodies)
  - Credentials, access tokens, API keys, connection strings
  - Full URLs with query strings that can carry personal data
consent:
  category: analytics                  # the consent purpose that gates collection, per your consent platform
  denied_behaviour: Name every destination and what it receives when consent is denied (row stored, not sent, or cookieless request)
  ga4_consent_mode: basic              # basic (no request while denied) | advanced (cookieless request carrying the denied state) | not_used
  server_side_enforcement: Where server events read the stored consent state and which destinations it filters; server routes bypass the browser consent platform
  withdrawal_propagation: How a withdrawal reaches each route and from when; deletion of data already sent follows the privacy owner's process

# Time and delivery
time:
  occurred_at: UTC timestamp at the source when the fact became true
  received_at: UTC timestamp assigned at ingestion; never supplied by the client
  late_arrival_policy: Counted in event-time reports after the next refresh; workflows ignore events older than <N hours>
delivery:
  retry: Same event_id, message_id and occurred_at on every retry
  dedupe_rule: Warehouse drops transport duplicates on message_id, then keeps one canonical row per event name plus event_id
  dual_emission: none                  # or the overlap window and the rule that stops a metric counting both

# Per-destination mapping. Only destinations with a named use. Check limits live (references/destination-mapping.md).
# via: direct_sdk | segment | rudderstack | measurement_protocol | gtm | warehouse_sync
# dedupe_mapping: the configured field that carries message_id, confirmed on one received event
destinations:
  warehouse:
    name: object_action_past_tense
    via: segment
    use: Canonical record for metrics
  ga4:
    name: object_action_past_tense     # or a recommended event when the meaning matches
    via: gtm                           # dataLayer push; write the push next to the mapping
    parameters: {example_enum_property: example_enum_property, event_id: event_id}
    key_event: false                   # when true, marked in Admin before the first release
    dedupe_mapping: none for custom events; prevent repeats at the source. event_id is a parameter for export joins, not a custom dimension
    use: Acquisition reporting
  mixpanel:
    name: object_action_past_tense
    via: direct_sdk
    dedupe_mapping: $insert_id set from message_id (36 bytes or less, letters, digits and hyphens)
    identity: $device_id before login; $device_id and $user_id after
    time: occurred_at
    use: Product funnels
  amplitude:
    name: object_action_past_tense
    via: segment
    dedupe_mapping: Segment Amplitude (Actions) Log Event, Insert ID mapped from messageId
    identity: Device ID and User ID sources, as configured
    time: occurred_at as milliseconds since epoch
    use: Product funnels
  segment:
    name: object_action_past_tense
    via: direct_sdk                    # the Segment library on the emitter
    dedupe_mapping: messageId set from message_id
    event_version: 1                   # sent as context.protocols.event_version when Protocols versioning is used
  rudderstack:
    name: object_action_past_tense
    via: direct_sdk
    dedupe_mapping: messageId set from message_id
  hubspot:
    send: derived_property             # derived_property | custom_event | none
    via: warehouse_sync
    object: company                    # company | custom workspace object
    match_key: Unique-value custom property holding account_id; upsert on it, never match on domain alone
    rollup: How workspace-level facts combine on one record (earliest, sum, latest)
    property: object_first_completed_at
    use: Owner routing or lifecycle workflow

# Ownership
owners:
  business_owner: Role or name that decides what the event means
  implementation_owner: Team that emits and delivers it
  data_owner: Role that maintains counting rules and monitoring
privacy_review: not_required           # required when a field could identify a person or consent behaviour changes

# Acceptance tests (references/acceptance-tests.md).
# Where the evidence for each layer is read:
evidence_sources:
  emission: SDK debug log, network request or server log line, showing event_id and message_id
  collection: CDP source debugger or collector log
  destination: each mapped destination's live or debug view, recording the received dedupe field value
  warehouse: canonical table, one row per event name plus event_id
  use: the metric or workflow state the event should change
# Each case states the expected result per layer. A negative case may use one line
# that names every layer it was checked in.
acceptance:
  - id: T1
    case: Normal success
    evidence:
      emission: one call with event_id and message_id
      collection: one message
      destination: one event in each mapped destination, dedupe field equal to message_id
      warehouse: one row with all required properties and the correct account_id
      use: metric or workflow changes once
  - id: T2.1                           # one T2.n per must_not_fire item, same order
    case: must_not_fire 1, UI intent step only
    evidence: absent at every layer (emitter log, CDP debugger, each destination, warehouse); metric unchanged
  - id: T2.2
    case: must_not_fire 2, operation fails
    evidence: absent at every layer; the failure event, if planned, carries a controlled error_category
  - id: T2.3
    case: must_not_fire 3, success page refreshed
    evidence: absent at every layer for the refresh; one event in total
  - id: T2.4
    case: must_not_fire 4, delivery retried after a forced timeout
    evidence: two sends with one message_id in the emitter log; one message in the CDP; destination counts unchanged; one warehouse row
  - id: T3
    case: Two genuine occurrences
    evidence: two rows with different event_id values in every layer; a first-per-unit metric still counts the unit once
  - id: T4
    case: Prohibited field present in the source data
    evidence: field absent in every destination payload, including URL parameters
  - id: T5
    case: Companion or renamed event sent through the same CDP as the canonical event (when other_emitters or dual_emission is set)
    evidence: both messages appear in the CDP debugger with different message_id values and arrive in every mapped destination; both carry one event_id; the metric counts the occurrence once

links:
  ticket: ""
  staging_evidence: ""
  metric_definition: ""
changelog:
  - version: 1
    date: "YYYY-MM-DD"
    change: Created
    approved_by: ""
