Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kafka Events Contract (HH-Lion)

This document defines exactly what producers should send to Kafka for HH-Lion.

1) Topic you should produce to

  • Primary ingestion topic: hh.lion.interaction.events

    • Purpose: behavioral events used for feature computation, training data, and analytics.
    • Controlled by env var KAFKA_EVENTS_TOPIC (default: hh.lion.interaction.events).
    • Where NAMESPACE prefixing is enabled, the effective topic becomes <NAMESPACE>.hh.lion.interaction.events.
  • Do not produce application events directly to recsys.impressions unless you explicitly want to override HH-Lion behavior.

    • HH-Lion emits homepage_impression to KAFKA_IMPRESSION_TOPIC (default: recsys.impressions) from POST /v2/homepage.
  • Dead-letter topic (hh.lion.interaction.events.dlq) is internal and receives invalid events from the consumer.

2) Required behavioral event types (hh.lion.interaction.events)

You should send these canonical event_type values:

  1. impression
  2. click
  3. view
  4. favorite_added
  5. package_selected
  6. begin_checkout
  7. booking_confirmed
  8. search

Source of truth:

  • specs/003-clickhouse-feature-store/contracts/interaction_event.avsc
  • recsys/ingestion/quality.py

3) Required data (all fields and constraints)

Always required for every event

FieldTypeRules
event_idstringMust be UUIDv4
event_typeenumMust be one of the 8 event types above
event_timestamplong (epoch ms)Must be int millis; default quality window: not older than 7 days, not more than 10 minutes in future
anonymous_idstringMust be Nano ID (21 chars, URL-safe alphabet)
session_idstringMust be Nano ID (21 chars, URL-safe alphabet)
context.languagestringISO 639-1 (2 lowercase letters), e.g. en, th
context.deviceenummobile, tablet, desktop
context.referrer_categoryenumorganic_search, paid_search, social, direct, partner, unknown

Optional/common fields

FieldTypeNotes
user_idstring | nullLogged-in member id (null for guests)
context.geostring | nullPattern CC-CITY_ID, e.g. TH-1
strategy_idstring | nullRanking strategy identifier
experiment_idsstring[] | nullActive experiment ids/variants
discountdecimal | nullAllowed only for package events (package_selected, begin_checkout, booking_confirmed)

Conditionally required fields by event type

FieldRequired whenExtra constraints
item_key, item_type, item_idAll item-scoped events: impression, click, view, favorite_added, package_selected, begin_checkout, booking_confirmeditem_key must equal item_type:item_id; item_type enum: restaurant, tag, link
section_id, section_type, positionimpression, clickposition >= 1
request_id, surfaceimpression, click, view, searchsurface enum: homepage, search, detail, collection, unknown
package_idpackage_selected, begin_checkout, booking_confirmeditem_type must be restaurant
search_querysearchMax length 256
booking_valuebooking_confirmedDecimal, must be > 0
price_shown, price_shown_currency, inventory_state, party_size, booking_timestamppackage_selected, begin_checkoutprice_shown_currency is ISO 4217 (3 chars); party_size >= 1
price_tier, cuisinebooking_confirmed onlyprice_tier enum: budget, mid, premium

Important negative rules

  • For non-item-scoped events (search), item_key, item_type, and item_id must be null/absent.
  • package_id is only allowed for package events (package_selected, begin_checkout, booking_confirmed).
  • search_query is only allowed for search.
  • booking_value is only allowed for booking_confirmed.
  • price_tier and cuisine are only allowed for booking_confirmed.

Section/item consistency

When section_type is one of:

  • restaurant_list => item_type must be restaurant
  • tag_list => item_type must be tag
  • link_list => item_type must be link

4) Minimal valid payload examples (all required event types)

[
  {
    "event_id": "550e8400-e29b-41d4-a716-446655440000",
    "event_type": "impression",
    "event_timestamp": 1764200000000,
    "anonymous_id": "V1StGXR8_Z5jdHi6B-myT",
    "session_id": "IRFa-VaY2b8A9xQ7mL0Pn",
    "item_key": "restaurant:1",
    "item_type": "restaurant",
    "item_id": "1",
    "section_id": "popular_near_you",
    "section_type": "restaurant_list",
    "position": 1,
    "request_id": "req-home-001",
    "surface": "homepage",
    "context": { "geo": "TH-1", "language": "en", "device": "mobile", "referrer_category": "direct" }
  },
  {
    "event_id": "550e8400-e29b-41d4-a716-446655440010",
    "event_type": "click",
    "event_timestamp": 1764200001000,
    "anonymous_id": "V1StGXR8_Z5jdHi6B-myT",
    "session_id": "IRFa-VaY2b8A9xQ7mL0Pn",
    "item_key": "restaurant:1",
    "item_type": "restaurant",
    "item_id": "1",
    "section_id": "popular_near_you",
    "section_type": "restaurant_list",
    "position": 1,
    "request_id": "req-home-001",
    "surface": "homepage",
    "context": { "geo": "TH-1", "language": "en", "device": "mobile", "referrer_category": "direct" }
  },
  {
    "event_id": "550e8400-e29b-41d4-a716-446655440020",
    "event_type": "view",
    "event_timestamp": 1764200002000,
    "anonymous_id": "V1StGXR8_Z5jdHi6B-myT",
    "session_id": "IRFa-VaY2b8A9xQ7mL0Pn",
    "item_key": "restaurant:1",
    "item_type": "restaurant",
    "item_id": "1",
    "request_id": "req-detail-001",
    "surface": "detail",
    "context": { "geo": "TH-1", "language": "en", "device": "mobile", "referrer_category": "direct" }
  },
  {
    "event_id": "550e8400-e29b-41d4-a716-446655440030",
    "event_type": "favorite_added",
    "event_timestamp": 1764200003000,
    "anonymous_id": "V1StGXR8_Z5jdHi6B-myT",
    "session_id": "IRFa-VaY2b8A9xQ7mL0Pn",
    "item_key": "restaurant:1",
    "item_type": "restaurant",
    "item_id": "1",
    "context": { "geo": "TH-1", "language": "en", "device": "mobile", "referrer_category": "direct" }
  },
  {
    "event_id": "550e8400-e29b-41d4-a716-446655440040",
    "event_type": "package_selected",
    "event_timestamp": 1764200004000,
    "anonymous_id": "V1StGXR8_Z5jdHi6B-myT",
    "session_id": "IRFa-VaY2b8A9xQ7mL0Pn",
    "item_key": "restaurant:1",
    "item_type": "restaurant",
    "item_id": "1",
    "package_id": "pkg-0001",
    "price_shown": "1200.00",
    "price_shown_currency": "THB",
    "inventory_state": "available",
    "party_size": 2,
    "booking_timestamp": 1764286400000,
    "context": { "geo": "TH-1", "language": "en", "device": "mobile", "referrer_category": "direct" }
  },
  {
    "event_id": "550e8400-e29b-41d4-a716-446655440050",
    "event_type": "begin_checkout",
    "event_timestamp": 1764200005000,
    "anonymous_id": "V1StGXR8_Z5jdHi6B-myT",
    "session_id": "IRFa-VaY2b8A9xQ7mL0Pn",
    "item_key": "restaurant:1",
    "item_type": "restaurant",
    "item_id": "1",
    "package_id": "pkg-0001",
    "price_shown": "1200.00",
    "price_shown_currency": "THB",
    "inventory_state": "available",
    "party_size": 2,
    "booking_timestamp": 1764286400000,
    "context": { "geo": "TH-1", "language": "en", "device": "mobile", "referrer_category": "direct" }
  },
  {
    "event_id": "550e8400-e29b-41d4-a716-446655440060",
    "event_type": "booking_confirmed",
    "event_timestamp": 1764200006000,
    "anonymous_id": "V1StGXR8_Z5jdHi6B-myT",
    "session_id": "IRFa-VaY2b8A9xQ7mL0Pn",
    "item_key": "restaurant:1",
    "item_type": "restaurant",
    "item_id": "1",
    "package_id": "pkg-0001",
    "booking_value": "1200.00",
    "price_tier": "mid",
    "cuisine": "japanese",
    "context": { "geo": "TH-1", "language": "en", "device": "mobile", "referrer_category": "direct" }
  },
  {
    "event_id": "550e8400-e29b-41d4-a716-446655440070",
    "event_type": "search",
    "event_timestamp": 1764200007000,
    "anonymous_id": "V1StGXR8_Z5jdHi6B-myT",
    "session_id": "IRFa-VaY2b8A9xQ7mL0Pn",
    "request_id": "req-search-001",
    "surface": "search",
    "search_query": "sushi omakase",
    "context": { "geo": "TH-1", "language": "en", "device": "mobile", "referrer_category": "organic_search" }
  }
]

Note: JSON examples use decimal values as strings for compatibility with common serializers before Avro encoding.

5) Secondary topic: recsys.impressions (emitted by HH-Lion API)

HH-Lion emits this automatically when homepage results are returned.

Schema source:

  • specs/018-recsys-audit-remediation/contracts/impression-event.avsc
  • recsys/shared/events.py

Top-level fields:

  • event_id, event_type, event_version
  • request_id, user_id, anonymous_id, session_id
  • sections[] (section_id, section_type, strategy, position, item_count)
  • experiment_assignments[] (flag_key, variant, assignment_timestamp)
  • context (geo, language, device, referrer_category)
  • metrics (latency_ms, fallback_triggered, cache_hit)
  • timestamp

6) Producer checklist

  1. Generate UUIDv4 for event_id, and Nano ID for anonymous_id and session_id.
  2. Send epoch milliseconds in event_timestamp and booking_timestamp.
  3. Ensure context.language, context.device, context.referrer_category are always present.
  4. Enforce event-specific required fields from section 3.
  5. Use canonical enums exactly as defined above (lowercase).