Docs, Event Data + Custom Event Parameter Page - #45
Conversation
…hetan/lin-236-accept-process-forward-custom-event-parameters-product_id
LIN-236 Accept, process & forward custom event parameters (product_id, order_id, etc.) to ad networks
BackgroundA customer reported that Meta flagged We want to adopt the AppsFlyer model — a single flat AppsFlyer Model (What We're Adopting)AppsFlyer uses a single flat key-value map for all event parameters: {
"event_name": "purchase",
"event_data": {
"product_id": "sku_12345",
"order_id": "order_98765",
"content_type": "shoes",
"num_items": 2,
"currency": "USD",
"amount": 49.99,
"payment_plan": "annual",
"coupon_code": "SAVE20"
}
}
Current Gap AnalysisCustom Event Flow (
|
| Step | File | Status |
|---|---|---|
SDK sends event_data JSON |
src/controllers/data.ts:987 |
OK — accepted |
Kafka message includes event_data |
src/controllers/data.ts:1039-1053 |
OK — forwarded |
Stored in Event.object |
src/services/event_service.ts:169-181 |
OK — stored |
| Postback dispatch | src/services/event_service.ts:186-194 |
BROKEN — only event_data.amount extracted, everything else dropped |
| Meta/Google/TikTok/Snapchat | postback handlers | No custom params arrive |
Payment Event Flow (POST /api/client/capture-payment)
| Step | File | Status |
|---|---|---|
| SDK sends payment data | src/controllers/data.ts:30-32 |
BROKEN — no metadata/event_data accepted from SDK at all |
| Kafka message | src/controllers/data.ts:99-117 |
No custom data included |
| Revenue record stored | src/services/payment_service.ts:200-217 |
metadata field exists in schema but never populated from API |
| Postback dispatch | src/services/payment_service.ts:264-274 |
BROKEN — only amountValue forwarded, no custom params |
| Meta/Google/TikTok/Snapchat | postback handlers | Only amount + currency arrive |
Ad Network Forwarding
| Network | File | Status |
|---|---|---|
| Meta CAPI | src/utils/meta/conversion_api.ts:781-784 |
Only sends {currency, value} in custom_data — no content_ids, order_id, etc. |
| Google Ads | src/utils/google_ads_api.ts:927-929 |
Sends empty body {} — app_event_data never populated |
| TikTok | src/services/postback_service.ts:824 |
No custom params forwarded |
| Snapchat | src/services/postback_service.ts:961 |
No custom params forwarded |
Standard Parameter Mapping
These standard params in event_data get mapped to each network's expected field names:
Customer sends in event_data |
Meta CAPI (custom_data) |
Google Ads (app_event_data) |
|---|---|---|
product_id |
content_ids: [product_id] |
item_id: [product_id] |
order_id |
order_id |
order_id |
content_type |
content_type ("product" or "product_group") |
content_type |
num_items |
num_items |
num_items |
contents (array of {id, quantity, item_price}) |
contents |
items |
search_string |
search_string |
search_string |
amount / value |
value (already works) |
value query param (already works) |
currency |
currency (already works) |
currency_code query param (already works) |
| Any other key | Spread into custom_data as custom property |
Passed through in app_event_data body |
Meta supports arbitrary custom properties in custom_data. Google accepts freeform JSON in the app_event_data request body.
Implementation Tasks
Task 1: Accept event_data in capturePayment API
File: src/controllers/data.ts:30-32
- Add
event_data(ormetadata) to the destructured request body - Include it in the Kafka message at lines 99-117
- The
PaymentDatainterface inpayment_service.ts:9-23already hasmetadata?: Record<string, any>— just needs to be populated
Task 2: Thread event_data through payment processing
File: src/services/payment_service.ts:264-274
- Pass
payment.metadatatoprocessEventForPostbacksas a neweventParamsargument
Task 3: Thread event_data through custom event processing
File: src/services/event_service.ts:186-194
- Currently only
event_data.amountis extracted:event.event_data?.amount ? Number(event.event_data.amount) : undefined - Pass the full
event.event_dataobject toprocessEventForPostbacks
Task 4: Update processEventForPostbacks and InAppEventPostback type
File: src/services/postback_service.ts:146-154
- Add
eventParams?: Record<string, any>to the method signature - Set
event_paramson theInAppEventPostbackobjects built at lines 285-323 and 335-374
File: src/utils/types/postback.ts
- Add to
InAppEventPostback:
// Custom event parameters (product_id, order_id, etc.) — AppsFlyer-style flat map
event_params?: Record<string, any>;Task 5: Forward to Meta CAPI
File: src/services/postback_service.ts — handleMetaPostback (line 492)
- When calling
trackPurchaseFromPayment(line 764) andtrackCustomEventFromTrigger(line 795), buildcustom_datafromdata.event_params:product_id->content_ids: [product_id]order_id->order_idcontent_type->content_typenum_items->num_itemscontents->contents- All other keys -> spread into
custom_data
File: src/utils/types/meta-integration.ts:76-84
- Add explicit standard fields to
CustomDatainterface:
export interface CustomData {
value?: number;
currency?: string;
content_name?: string;
content_category?: string;
content_ids?: string[];
content_type?: string;
contents?: Array<{ id: string; quantity: number; item_price?: number }>;
order_id?: string;
num_items?: number;
search_string?: string;
status?: string;
// Existing fields
description?: string;
level?: string;
max_rating_value?: number;
success?: boolean;
[key: string]: any;
}File: src/utils/meta/conversion_api.ts:781-784
trackPurchasecurrently buildscustomDataas only{currency, value}— merge incomingcustom_datafields into it
Task 6: Forward to Google Ads
File: src/utils/google_ads_api.ts:927-929
sendInAppEventcurrently sends empty body{}— populateapp_event_datafromdata.event_params:product_id->item_id: [product_id]- All other custom params -> pass through in
app_event_data
Task 7: Forward to TikTok and Snapchat
File: src/services/postback_service.ts
handleTikTokPostback(line 824) — passevent_paramsthrough to TikTok integrationhandleSnapchatPostback(line 961) — passevent_paramsthrough to Snapchat integration- Update respective integration files:
src/utils/tiktok/linkrunner-tiktok-integration.tssrc/utils/snapchat/linkrunner-snapchat-integration.ts
Task 8: Update docs.linkrunner.io
After all backend changes are deployed, update the public documentation at docs.linkrunner.io with the following:
8a. Update capture-event documentation
- Document that
event_datasupports a flat key-value map of custom parameters - List the standard recognized parameters and explain they get mapped to ad networks automatically:
product_id— Product SKU/identifier (forwarded to Meta ascontent_ids, Google asitem_id)order_id— Transaction/order identifiercontent_type—"product"or"product_group"num_items— Number of items in the transactioncontents— Array of{ id, quantity, item_price }for multi-product eventssearch_string— Search query text (for search events)
- Explain that any additional custom keys (e.g.
payment_plan,coupon_code,subscription_tier) are passed through as-is to ad networks - Add code examples for each SDK (Flutter, iOS, Android) showing how to send events with custom params
8b. Update capture-payment documentation
- Document the new
event_data/metadatafield accepted by the payment API - Show how to include
product_id,order_id, and other params alongsideamountandcurrency - Add code examples showing a purchase event with product-level data:
{
"payment_id": "pay_123",
"amount": 49.99,
"currency": "USD",
"event_data": {
"product_id": "sku_12345",
"order_id": "order_98765",
"content_type": "product",
"num_items": 1
}
}8c. Add a "Custom Event Parameters" reference page
- Dedicated page explaining the AppsFlyer-style flat map approach
- Full table of standard parameters with descriptions and which ad networks they map to
- Best practices:
- Always include
product_idfor purchase events (required by Meta for product catalog matching) - Include
order_idfor deduplication across ad networks - Use
content_type: "product"whenproduct_idrefers to a specific SKU - Revenue params (
amount,currency) are handled automatically — don't duplicate them inevent_data
- Always include
- Examples for common event types: purchase, add_to_cart, subscribe, custom events
Acceptance Criteria
-
capture-paymentAPI acceptsevent_data/metadatafrom SDK (flat key-value map, AppsFlyer-style) -
capture-eventforwards fullevent_datathrough the postback pipeline (not justamount) - Standard params (
product_id,order_id, etc.) are mapped to each ad network's expected field names - Arbitrary custom params beyond the standard set are passed through as-is
- Custom params reach Meta CAPI in
custom_data(withcontent_ids,order_id, etc.) - Custom params reach Google Ads in
app_event_datarequest body - Custom params reach TikTok and Snapchat via their respective CAPI
- Existing amount/currency revenue tracking continues working unchanged
- No DB migrations needed —
Event.objectandRevenue.metadataalready store JSON - docs.linkrunner.io updated:
capture-eventdocs,capture-paymentdocs, and new "Custom Event Parameters" reference page
Notes
- No breaking changes —
event_datais already accepted bycapture-event; adding it tocapture-paymentis additive - No schema migration —
Event.object(JSON) andRevenue.metadata(JSON) already exist - Meta requires events within 7 days — existing validation handles this
- Google
app_event_datais freeform JSON — no strict schema - Adjust uses a different model (callback_params vs partner_params) — we're going with AppsFlyer's simpler single-map approach
|
No actionable comments were generated in the recent review. 🎉 📝 WalkthroughWalkthroughAdds a new Custom Event Parameters API doc, standardizes Changes
Sequence Diagram(s)sequenceDiagram
participant App as User App (SDK)
participant API as Event Capture API
participant DB as Database
participant Meta as Meta CAPI
participant Google as Google Ads
participant TikTok as TikTok
participant Snap as Snapchat
rect rgba(200,230,255,0.5)
App->>API: Send event (event_name, event_data, eventId?)
end
rect rgba(220,255,200,0.5)
API->>DB: Store event, event_data, eventId
API-->>API: Auto-map standard keys to network params
end
rect rgba(255,240,200,0.5)
API->>Meta: Forward mapped + passthrough custom params
API->>Google: Forward mapped + passthrough custom params
API->>TikTok: Forward mapped + passthrough custom params
API->>Snap: Forward mapped + passthrough custom params
end
API->>App: Acknowledge receipt (dedup info)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
api-reference/event-capture.mdx (1)
35-45: Consider addingevent_idto the request example.It’s newly documented and showing it in the sample payload makes dedup usage more discoverable.
Proposed update
{ "event_name": "purchase_completed", "event_data": { "product_id": "ABC123", "order_id": "ORD-12345", "content_type": "product", "num_items": 2, "amount": 149.99, "currency": "USD" }, - "user_id": "user_12345" + "user_id": "user_12345", + "event_id": "550e8400-e29b-41d4-a716-446655440000" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api-reference/event-capture.mdx` around lines 35 - 45, The sample JSON payload is missing an event_id field which helps demonstrate deduplication; update the example payload to include an "event_id" key at the top level (alongside "event_name" and "user_id") with a representative value (e.g., a UUID or unique string) so readers see how to pass dedup keys—locate the JSON block containing "event_name", "event_data", and "user_id" and insert "event_id": "<unique-id>" at the same level.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api-reference/revenue-tracking.mdx`:
- Around line 48-56: The parameter table omits whether the amount field is
required; update the row for the amount parameter (the "amount" column entry) to
include its requirement status to match others (e.g., prefix with "Required." or
"Optional."), ensuring the description reads like the other rows (for example:
"Required. Payment amount. Send in one currency only"); leave other parameter
rows unchanged.
---
Nitpick comments:
In `@api-reference/event-capture.mdx`:
- Around line 35-45: The sample JSON payload is missing an event_id field which
helps demonstrate deduplication; update the example payload to include an
"event_id" key at the top level (alongside "event_name" and "user_id") with a
representative value (e.g., a UUID or unique string) so readers see how to pass
dedup keys—locate the JSON block containing "event_name", "event_data", and
"user_id" and insert "event_id": "<unique-id>" at the same level.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Rewrite custom-event-parameters.mdx for clarity: add "why" section, standard parameters table, Steps component, Notes/Warnings/Tips, contextual example descriptions, and related page links - Add CLAUDE.md with docs writing conventions for consistency - Fix LinkRunner → Linkrunner in prose text across SDK pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (4)
sdk/flutter.mdx (2)
746-749: Consider adding eventData to the complete example for consistency.The complete example passes
eventIdbut omitseventData. While valid, including an eventData map (even empty) would better demonstrate the full signature and maintain consistency with the detailed examples earlier in the document.📝 Suggested improvement
await LinkRunner().trackEvent( eventName: 'button_clicked', + eventData: {'screen': 'home'}, eventId: '550e8400-e29b-41d4-a716-446655440000', // Optional: Unique event identifier (String) for deduplication );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/flutter.mdx` around lines 746 - 749, Update the complete example call to LinkRunner().trackEvent to include the eventData parameter so it matches the full signature shown elsewhere; specifically add an eventData map (can be empty or with an example key/value) alongside eventName and eventId in the call to LinkRunner().trackEvent to demonstrate the full usage.
767-768: Consider using 'Setup' instead of 'Set Up' in the heading.Based on learnings, the repository prefers using the single-word form 'Setup' over the phrasal verb 'Set Up' in headings for consistency.
📝 Suggested heading change
- <Card title="Set Up Deep Linking" icon="link" href="/features/deep-linking"> + <Card title="Setup Deep Linking" icon="link" href="/features/deep-linking">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/flutter.mdx` around lines 767 - 768, Change the Card title prop from the phrasal verb to the single-word form by updating the Card with title="Set Up Deep Linking" to title="Setup Deep Linking" (the Card component and its title prop are the unique identifiers to edit).sdk/react-native.mdx (1)
339-340: Consider using 'Setup' instead of 'Set Up' in the heading.Based on learnings, the repository prefers using the single-word form 'Setup' over the phrasal verb 'Set Up' in headings for consistency.
📝 Suggested heading change
- <Card title="Set Up Deep Linking" icon="link" href="/features/deep-linking"> + <Card title="Setup Deep Linking" icon="link" href="/features/deep-linking">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/react-native.mdx` around lines 339 - 340, Update the Card component title prop to use the single-word form "Setup" for consistency: change the title value on the Card (currently "Set Up Deep Linking") to "Setup Deep Linking" so the heading matches the repository style.sdk/android.mdx (1)
631-632: Consider using 'Setup' instead of 'Set Up' in the heading.Based on learnings, the repository prefers using the single-word form 'Setup' over the phrasal verb 'Set Up' in headings for consistency.
📝 Suggested heading change
- <Card title="Set Up Deep Linking" icon="link" href="/features/deep-linking"> + <Card title="Setup Deep Linking" icon="link" href="/features/deep-linking">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/android.mdx` around lines 631 - 632, Change the Card component title from the phrasal form to the single-word form by updating the title prop value in the Card with title="Set Up Deep Linking" to title="Setup Deep Linking"; locate the Card element (the JSX/Markdown fragment with Card title="Set Up Deep Linking" icon="link" href="/features/deep-linking") and replace the title string accordingly to maintain repository heading consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@sdk/android.mdx`:
- Around line 631-632: Change the Card component title from the phrasal form to
the single-word form by updating the title prop value in the Card with
title="Set Up Deep Linking" to title="Setup Deep Linking"; locate the Card
element (the JSX/Markdown fragment with Card title="Set Up Deep Linking"
icon="link" href="/features/deep-linking") and replace the title string
accordingly to maintain repository heading consistency.
In `@sdk/flutter.mdx`:
- Around line 746-749: Update the complete example call to
LinkRunner().trackEvent to include the eventData parameter so it matches the
full signature shown elsewhere; specifically add an eventData map (can be empty
or with an example key/value) alongside eventName and eventId in the call to
LinkRunner().trackEvent to demonstrate the full usage.
- Around line 767-768: Change the Card title prop from the phrasal verb to the
single-word form by updating the Card with title="Set Up Deep Linking" to
title="Setup Deep Linking" (the Card component and its title prop are the unique
identifiers to edit).
In `@sdk/react-native.mdx`:
- Around line 339-340: Update the Card component title prop to use the
single-word form "Setup" for consistency: change the title value on the Card
(currently "Set Up Deep Linking") to "Setup Deep Linking" so the heading matches
the repository style.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This reverts commit daec621.
Summary by CodeRabbit
Documentation
New Features