Connect Segment
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/segment-destination); the Segment catalog
listing is pending the Actions partner review.
Route the events you already collect with Segment into Tessera as a Segment destination (Actions framework, cloud mode) — no new instrumentation. Three actions with default subscriptions do the work:
| Action | Subscription | Canonical event |
|---|---|---|
| Track Event | type = "track" | event name → event_type (normalized), properties → attributes |
| Identify Entity | type = "identify" | profile.updated with traits as attributes |
| Page View | type = "page" | page.viewed with page properties as attributes, URL in context |
alias and group calls are acknowledged and ignored (identity resolution
ships in Phase 3), and Segment's messageId becomes the event's idempotency
id — Segment's own at-least-once retries can never double-count.
1. Create the credential (least privilege)
Mint a dedicated ingest-scope API key for Segment under Project
Settings → API keys — not your admin key. If the destination is ever
compromised it can only write events, never read data or manage the project.
The key is pasted once into the destination's Tessera API Key setting
(stored as a Segment secret) and only ever sent as the Authorization
bearer to https://ingest.staging.tessera.zone/v1/events.
2. Record the source in Tessera
A streaming source records the integration in your project (timeline attribution + dead-letter scoping):
{
"name": "segment-prod",
"kind": "stream",
"config": {
"subtype": "adapter",
"adapter": "segment",
"entity_type": "user"
}
}
3. Add the destination in Segment
In the Segment app: Catalog → Destinations → Tessera → connect it to the sources you want routed. Settings:
| Setting | Value |
|---|---|
| Tessera 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 |
Enable the three pre-subscribed actions. Batching is on: the destination
respects the ingestion limits (500 events / 5 MiB per request) and honors
retry_after_seconds on rate limits by retrying — per-event failures show
up in Segment's event delivery view without failing whole batches.
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
Send one track from Segment's Event Tester, 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 disabled for the source | enable per-source routing in Segment |
event dropped, delivery view says dropped_no_entity_id | neither userId nor anonymousId on the call | send an id; Tessera never fabricates one |
event dropped, delivery view says 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 dropped, dropped_invalid_event_type | event name can't normalize to ^[a-z][a-z0-9_.]* form | 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 |
| duplicated-looking events | different messageIds upstream | dedupe upstream or supply your own id property |