---
title: "Record business facts on the server · Agent Wranglers"
url: https://agentwranglers.se/playbook/data/product-analytics/server-side-business-events
updated: 2026-10-20
licence: "All rights reserved"
---

# Record business facts on the server

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Product analytics](https://agentwranglers.se/playbook/data/product-analytics)
- Rule: Record events that count money, sign-ups or other business facts on the server, at the moment the fact is committed. Use browser events only for behaviour in the page.
- How: Ad blockers, closed tabs and failed networks drop browser events, and a reload can send one twice. Emit `order_completed` from the code that commits the order, or from the confirmed payment webhook, never from the thank-you page. Write it to an [outbox](https://agentwranglers.se/playbook/data/transactions-and-concurrency/outbox) in the same transaction, so a rolled-back order is never counted. Give each event an ID derived from the fact, such as the order ID, and drop duplicates on that ID. Stamp it with the time the fact happened, and compare counts by that time, not by arrival.
- Check: Place 20 test orders with an ad blocker on. The events table holds exactly 20 `order_completed` rows, matching the orders table. A daily query compares both counts and alerts on any difference.

Example:

```sql
-- Daily reconciliation: must return 0 rows
SELECT day, o.orders, e.events FROM
  (SELECT (created_at AT TIME ZONE 'UTC')::date AS day, count(*) AS orders FROM orders GROUP BY 1) o
  FULL JOIN (SELECT (occurred_at AT TIME ZONE 'UTC')::date AS day, count(DISTINCT event_id) AS events
             FROM events WHERE name = 'order_completed' GROUP BY 1) e USING (day)
WHERE o.orders IS DISTINCT FROM e.events AND day < current_date - 1;
```

```js
await db.transaction(async tx => {
  const order = await tx.orders.insert(cart);
  await tx.outbox.insert({ kind: 'analytics', payload: { event_id: `order-${order.id}`, name: 'order_completed', plan: order.plan, amount_cents: order.totalCents } });
});
```

```python
with db.transaction() as tx:
    order = tx.orders.insert(cart)
    tx.outbox.insert(kind="analytics", payload={
        "event_id": f"order-{order.id}", "name": "order_completed",
        "plan": order.plan, "amount_cents": order.total_cents})
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers data playbook: https://agentwranglers.se/playbook/data/product-analytics/server-side-business-events`.

- Previous: [Keep personal data out of events](https://agentwranglers.se/playbook/data/product-analytics/events-without-personal-data)
- Next: [Define each metric once](https://agentwranglers.se/playbook/data/product-analytics/metric-definitions)
- [Product analytics](https://agentwranglers.se/playbook/data/product-analytics)
