Short answer, This is expected. A single TransactionEvent (manual or automatic) is stored as three entries in the events table that share the same transactionId:
- Root summary event (eventLevel = 0): transaction metadata and totals.
- Sub-event for productsSpent (eventLevel = 1): items/currency spent.
- Sub-event for productsReceived (eventLevel = 1): items/currency received.
Why it works this way Unity Analytics normalizes transactions so you can:
- Query overall transaction totals and metadata efficiently (root summary).
- Aggregate item-level details separately (spent/received sub-events). This structure is identical whether you use automatic IAP reporting or call the manual RecordTransactionEvent API.
What you’ll see in the events table
- One “summary” row with totals and transaction info (no product arrays).
- One row listing what was spent (productsSpent).
- One row listing what was received (productsReceived). All three rows share the same transactionId and differ by eventLevel.
How to query (examples)
- Count distinct transactions (use summary rows only):
SELECT COUNT(*) AS total_transactions
FROM EVENTS
WHERE EVENT_NAME = 'transaction'
AND EVENT_JSON:eventLevel::INT = 0;
- Sum revenue (avoid double counting; use summary rows only):
SELECT
SUM(EVENT_JSON:convertedProductAmount::NUMBER) / 100 AS total_revenue
FROM EVENTS
WHERE EVENT_NAME = 'transaction'
AND EVENT_JSON:transactionType::STRING = 'PURCHASE'
AND COALESCE(EVENT_JSON:revenueValidated::INT, 0) = 1;
- List items spent (sub-events):
SELECT
EVENT_JSON:transactionID::STRING AS transaction_id,
EVENT_JSON:productID::STRING AS product_id,
EVENT_JSON:productCategory::STRING AS product_category,
EVENT_JSON:productType::STRING AS product_type,
EVENT_JSON:productAmount::NUMBER AS product_amount,
EVENT_JSON:realCurrencyType::STRING AS currency,
EVENT_JSON:realCurrencyAmount::NUMBER AS currency_amount
FROM EVENTS
WHERE EVENT_NAME = 'transaction'
AND EVENT_JSON:eventLevel::INT = 1
AND EVENT_JSON:transactionVector::STRING = 'SPENT';
- List items received (sub-events):
SELECT
EVENT_JSON:transactionID::STRING AS transaction_id,
EVENT_JSON:productID::STRING AS product_id,
EVENT_JSON:productCategory::STRING AS product_category,
EVENT_JSON:productType::STRING AS product_type,
EVENT_JSON:productAmount::NUMBER AS product_amount,
EVENT_JSON:realCurrencyType::STRING AS currency,
EVENT_JSON:realCurrencyAmount::NUMBER AS currency_amount
FROM EVENTS
WHERE EVENT_NAME = 'transaction'
AND EVENT_JSON:eventLevel::INT = 1
AND EVENT_JSON:transactionVector::STRING = 'RECEIVED';
Common pitfalls
- “It looks like the event sent three times.” It didn’t—those are the summary + spent + received entries by design.
- Double-counting totals. Always use eventLevel = 0 rows for revenue/transaction totals.
- Mismatched joins. When combining summary and sub-events, join on transactionId and filter by eventLevel.
Notes
- The three-part structure applies to both manual and automatic transaction reporting.
- Use transactionId to correlate the rows, and eventLevel to distinguish summary vs item-level detail.