Connect RudderStack
Planned for Phase 2. This guide describes the planned setup; the configuration shown below is validated against the current API schema so it stays accurate.
The destination is implemented and contract-tested in the monorepo
(integrations/cdp/rudderstack-destination); the RudderStack
catalog listing is pending their integrations-repo review.
Route events from RudderStack into Tessera as a cloud destination
(server-side; no device mode). The destination transformer maps
track/identify/page calls onto canonical events with exactly the
same mapping as the planned Segment destination — both call the
one shared implementation, governed by the same template document — and
batches them to POST /v1/events (500 events / 5 MiB per request).
alias and group calls are acknowledged and ignored (identity resolution
ships in Phase 3), and RudderStack's messageId becomes the event's
idempotency id, so retries never double-count.
1. Create the credential (least privilege)
Mint a dedicated ingest-scope API key for RudderStack under Project
Settings → API keys. Paste it once into the destination's API Key
setting — RudderStack stores it as a secret and it is only ever sent as the
Authorization bearer to https://ingest.staging.tessera.zone/v1/events.
2. Record the source in Tessera
{
"name": "rudderstack-prod",
"kind": "stream",
"config": {
"subtype": "adapter",
"adapter": "rudderstack",
"entity_type": "user"
}
}
3. Add the destination in RudderStack
In the RudderStack dashboard: Destinations → New destination → Tessera, connect your sources, and fill the settings:
| Setting | Value |
|---|---|
| API Key | the ingest-scope key from step 1 (secret) |
| Mapping Template | optional JSON template (below); empty = default |
| Ingestion URL | leave default unless you run a single-tenant deployment |
Delivery semantics: rate limits (429 + retry_after_seconds) and
transient errors are retryable — RudderStack redelivers and Tessera
deduplicates on messageId; per-event rejections surface individually in
the delivery view without failing the batch; intentionally ignored calls
(alias/group, calls with no usable entity id) are marked filtered, not
failed, with the counted reason.
4. The mapping template
Both planned CDP destinations translate calls with the same versioned mapping
template (JSON Schema id https://spec.tessera.dev/schemas/cdp-mapping.json,
in the monorepo at api/jsonschema/cdp-mapping.json). Paste a template
document into the destination's Mapping Template setting — or leave it
empty for the default:
{ "version": 1, "entity_type": "user" }
Every knob, with defaults:
{
"version": 1,
"entity_type": "user",
"entity_id_precedence": ["user_id", "anonymous_id"],
"identify_event_type": "profile.updated",
"page_event_type": "page.viewed",
"event_name_rules": {
"lowercase": true,
"space_replacement": "_",
"renames": { "Match Finished": "match.finished" }
},
"attribute_renames": { "order_total": "amount" },
"attribute_drops": ["internal_note"],
"flatten_separator": "_"
}
| Field | Meaning |
|---|---|
version | Always 1 (the template document is versioned). |
entity_type | The entity type your events describe (user, account, player, …). |
entity_id_precedence | Which CDP id becomes the entity id: first non-empty of user_id (userId) / anonymous_id (anonymousId). Calls with neither are dropped and counted — never sent with a fabricated id. A numeric id becomes its digits (12 and 12.0 are 12, as on every other connector, see Entities and events); an id that is not an identifier (a fractional number, a boolean, an object) drops the call, counted as dropped_invalid_entity_id, and never falls back to the other id. |
identify_event_type | Event type for identify calls (traits become attributes). |
page_event_type | Event type for page calls (page properties become attributes; the URL rides in the non-model context). |
event_name_rules | How track names become event types: renames win (exact match, target used verbatim); otherwise lowercase (if lowercase) and whitespace runs become space_replacement. Names that still don't match ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){0,3}$ are dropped with a counted reason. |
attribute_renames | Renames applied to flattened keys before normalization. |
attribute_drops | Flattened keys to drop deliberately. |
flatten_separator | Joins nested property paths: order.total → order_total. |
Translation rules that are not configurable:
event_idis always the CDPmessageId, so CDP-side retries deduplicate downstream — redelivery is safe by construction.- The event
timestampis the CDP's event timestamp (timestamp, falling back tooriginalTimestamp) — neversentAt/receivedAtdelivery time, and never invented when absent. - CDP context (library, user agent, IP, page URL) maps into the canonical
event's non-model
context, never intoattributes. - Attribute keys normalize to
^[a-z][a-z0-9_]{0,63}$(lowercase, invalid characters become_); key collisions keep the first value; arrays keep at most 64 string/number elements; arrays of objects are dropped. aliasandgroupcalls are acknowledged and ignored (identity resolution ships in Phase 3).
CDP-sourced attributes pass through schema-contract review like every other source — deny by default: connecting a CDP does not by itself make anything a model input.
5. Test and verify
Use RudderStack's Live Events view to send one track, then read the
entity's timeline (GET /entities/user/{id}/events). Contract-time
violations park in the dead-letter view,
replayable after a contract fix.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| events missing | destination not connected to the source | connect the source in the RudderStack dashboard |
event filtered with dropped_no_entity_id | neither userId nor anonymousId on the call | send an id; Tessera never fabricates one |
event filtered with dropped_invalid_entity_id | the first id present (userId, else anonymousId) is not an identifier: a fractional number (12.5), a boolean, an object, or a whole number above 2^53 − 1 sent as a JSON number (the rule) | send the id as a string; the other id is never used instead |
event filtered with dropped_invalid_event_type | event name can't normalize to a valid event type | add an event_name_rules.renames entry for that name |
alias/group not appearing | acknowledged and ignored in v1 | expected — identity resolution is Phase 3 |
| traits missing on identify | traits nested under context.traits only | supported — both trait locations map identically |