Identify customers
Use one stable customer identity across errors, traces, logs, replay, events, and AI calls.
On this page
Identity is what turns separate telemetry into a customer story. Anectico attaches a distinct_id to
each relevant signal and resolves aliases to one canonical customer at query time.
A known person has an identifier Anectico resolved to a person. An unmapped or missing identifier is unattributed, even if its text equals another person's person ID. Its events and costs still count in traffic totals. Reads that show unidentified visitors keep them separate from known people. An anonymous person that Anectico resolved is different from an unresolved identifier.
These reads are as reliable as your application's own identification of customers. Anyone holding an intake key can declare a known identifier; telemetry does not authenticate each end user. Use stable IDs your application controls and protect server intake credentials.
The same rule applies to customer IDs sent for flag targeting, product experiment assignment and survey delivery. A person ID alone does not prove a customer-ID mapping. Identify the application-owned ID first; an unresolved visitor's survey answers remain in the unresolved population, and cannot become another person's.
Choose a stable ID
Use an application-owned identifier that does not change when someone updates their email or display name. A database user ID is usually the right choice.
good: user_8842
avoid: buyer@acme.example
Email and name belong in customer properties. A name stays searchable there. An email address does
not: Anectico redacts email addresses in properties when it receives them, so the stored value is
[REDACTED] and a search by the address finds nobody. To find a customer, search by the stable ID.
If your team must look customers up by email inside Anectico, the only way is to use the email as
the ID, with the cost described above. No setting stores an email property in readable form.
What Anectico redacts and withholds lists everything that is replaced this way, and what
that means for the rest of your instrumentation.
Identify after authentication
Call the SDK's identify method after your application knows who the customer is.
Identify and group admission requires a browser_capture, mobile_capture or server_capture key with
analytics:write; OTLP requires a separate native_ingest key with ingest:write.
For the JavaScript core client, set captureApiKey
(ANECTICO_CAPTURE_API_KEY in Node) alongside the native key in apiKey. Missing capture
configuration throws before identity changes; denied product consent remains a no-op. Standalone
analytics clients take the capture key in their own key option. Go uses WithCaptureAPIKey and
Python uses capture_api_key, or ANECTICO_CAPTURE_API_KEY in either SDK. Missing, malformed
or shared capture credentials return a Go error or raise Python ValueError before identity
changes or requests; this also applies to Python sync_person. See
native intake keys for current SDK rollout limits.
JavaScript / TypeScript
anectico.identify('user_8842', {
email: 'buyer@acme.example',
plan: 'pro',
});
Python
client.identify('user_8842', properties={
'email': 'buyer@acme.example',
'plan': 'pro',
})
Go
if err := client.Identify(ctx, "user_8842", map[string]any{
"email": "buyer@acme.example",
"plan": "pro",
}); err != nil {
return err
}
Swift (iOS)
Anectico.identify("user_8842", set: [
"email": "buyer@acme.example",
"plan": "pro",
])
Kotlin (Android)
Anectico.identify(
"user_8842",
mapOf("email" to "buyer@acme.example", "plan" to "pro"),
)
React Native
await Anectico.identify('user_8842', {
email: 'buyer@acme.example',
plan: 'pro',
});
Flutter
await Anectico.identify('user_8842', set: {
'email': 'buyer@acme.example',
'plan': 'pro',
});
The browser and mobile SDKs keep an anonymous ID before login. Identifying a customer carries that prior ID so Anectico can attach the pre-login activity to the known customer.
Anectico only promotes that prior ID while it still belongs to an anonymous person. If a new stable ID arrives with a prior ID that already belongs to another identified customer, Anectico keeps the two customers separate and leaves the existing alias with its original owner. This conservative boundary prevents recycled identifiers or stale shared-device state from combining unrelated customer histories.
Out-of-order profile sources
Anectico resolves customer-property conflicts independently per key using the capture
event's source timestamp, with message_id as the deterministic equal-time
tie-break. A delayed older CRM update therefore cannot overwrite a newer browser
or server update. JSON null is stored as an explicit value; deletion requires
the raw capture wire's unset field.
High-level identify() methods send ordinary set updates at the SDK's current
time. For a batch importer that must preserve an external source timestamp, use
the direct customer-property capture contract.
Send an ordinary named event when its source payload should be visible in the
customer timeline; the reserved identify event links identity and mutates the
profile but does not create a timeline event.
Reset on logout
Always reset identity when a user logs out or when a shared device changes users.
JavaScript / TypeScript
anectico.reset();
Python
client.reset()
Go
if err := client.Reset(ctx); err != nil {
return err
}
Swift (iOS)
Anectico.reset()
Kotlin (Android)
Anectico.reset()
React Native
await Anectico.reset();
Flutter
await Anectico.reset();
Resetting establishes a new user boundary: it rotates the SDK's anonymous identity and session where
applicable and clears groups, global error-user context, and uncaptured breadcrumbs. In the
JavaScript browser SDK, it first stops active replay recorders and flushes their buffered prior-user
tail; call startReplay() again after identifying the next user if recording should resume. Other
already-captured telemetry remains queued with its original attribution; only mutable context that
could otherwise attach to a later user's signals is discarded.
Web and mobile flag and survey identity
Identify web, iOS and Android with the same stable signed-in ID in the same project. Keep each
installation's anonymous ID and session separate. The same text in another project is another
person. Admission of an identify call is local; alias joining happens asynchronously. Read
search_persons or anectico persons search user_8842 --json and check the exact anonymous and
signed-in aliases before reading the timeline. Then verify each originating event ID and session;
two same-named events on a timeline alone do not establish exact delivery.
Changed exact identity clears groups and same exact identity preserves them. Rejected local identity admission leaves the old identity and groups. Admitted reset rotates anonymous identity and session. Current native clients refuse these analytics mutations without collection consent or a capture key. Restoring consent does not replay previously refused calls.
Flag evaluation can continue under an independently declared current binding, without telemetry
mutation; missing binding returns identity_unavailable. Replacement, clearing or withdrawal
invalidates stale work. A canonical person joins evidence but does not rebucket flags under a
shared ID: different anonymous aliases or contexts may receive different assignments. Compare
variants only for the same current distinct ID, context and definition version.
The native clients preserve source sessions on survey retries and the server propagates them; verify stored correlation for your runtime and deployment. Original per-exposure public attribution remains unqualified. React Native and Flutter have typed native delivery bridges; verify the linked runtime you use independently. See mobile setup and verification for exact source APIs, consent and evidence limits.
Scope identity per request on servers
A backend handles many customers concurrently. Do not switch a process-wide identity for every request. Use request context or framework middleware instead.
JavaScript / TypeScript
import { context } from '@opentelemetry/api';
import { contextWithDistinctId } from '@anectico/sdk';
const requestContext = contextWithDistinctId(context.active(), user.id);
await context.with(requestContext, () => handleRequest());
Python
from anectico import reset_context_distinct_id, set_context_distinct_id
token = set_context_distinct_id(user.id)
try:
handle_request()
finally:
reset_context_distinct_id(token)
Go
requestContext := anectico.ContextWithDistinctID(r.Context(), user.ID)
The request-scoped identity should be active before creating spans, capturing errors, or writing logs.
The JavaScript product-event client does not read that context. A server that captures product
events for many customers builds one AnalyticsClient per request, with an IdentityManager and a
SessionManager of its own, as the
JavaScript reference shows.
Resetting that client ends only that request's identity and session.
Propagate only across trusted boundaries
Anectico uses W3C traceparent and baggage to continue a trace and customer identity across services.
Treat incoming baggage from the public internet as untrusted. Enable identity extraction only
behind a gateway that removes client-supplied baggage and recreates it from the authenticated
session.
Apply the same rule to paired account baggage. Resolve both account coordinates from the authenticated request before creating the inbound server span. A caller-supplied distinct ID or account stamp describes telemetry; it never grants access or proves that attribution is correct.
Allow outbound propagation only to services you control. Do not send customer identity baggage to arbitrary third-party URLs.
Associate customers with accounts
For B2B applications, add group membership after identifying the customer:
JavaScript / TypeScript
anectico.group('company', 'acme', { name: 'Acme Inc' });
Python
client.group('company', 'acme', {'name': 'Acme Inc'})
Go
if err := client.Group(ctx, "company", "acme", map[string]any{
"name": "Acme Inc",
}); err != nil {
return err
}
Swift (iOS)
Anectico.group(type: "company", key: "acme", set: ["name": "Acme Inc"])
Kotlin (Android)
Anectico.group("company", "acme", mapOf("name" to "Acme Inc"))
React Native
await Anectico.group('company', 'acme', { name: 'Acme Inc' });
Flutter
await Anectico.group('company', 'acme', set: {'name': 'Acme Inc'});
This records that the current customer belongs to the account. Use an immutable account ID for the group key and a human-readable name as a property.
Across the SDKs, cached groups belong to the current distinct ID. Changing that ID with
identify() clears them, including when an anonymous visitor first signs in. Call group() after
identifying to associate the new customer's later events with an account. Identifying the same ID
again preserves the cache. Events already captured retain their original identity and groups,
including during retries; explicit per-event $groups still take precedence.
IDs are compared by their exact text: the JSON strings "caf\u00e9" and "cafe\u0301"
identify different customers even though they look the same. A group call started
for the previous customer cannot attach that account to the new customer's later events.
This applies to a Go Client's current identity too. The server-side Go Analytics.Identify
method and Python AnalyticsClient.sync_person synchronize the named person without changing
the process's current identity or groups.
Servers: name the account on each event
A browser or mobile app has one customer at a time, so its group call also attaches that
customer's later events to the account. A server handles many customers at once. In the Go and
Python SDKs, group for a named customer records the membership only; it does not follow that
customer's later events. Name the account on the event, or once per request, so account analytics
such as unique accounts, account funnels and account retention count it:
Go
buyer := anectico.WithDistinctID(userID)
events.Group(ctx, "company", companyID, map[string]any{"name": companyName}, buyer)
events.Capture(ctx, "purchased", map[string]any{"amount": 29}, buyer,
anectico.WithGroups(map[string]string{"company": companyID}))
// Or once per request, next to the request identity:
ctx = anectico.ContextWithGroups(ctx, map[string]string{"company": companyID})
Python
events.group('company', company_id, {'name': company_name}, distinct_id=user_id)
events.capture('purchased', {'amount': 29}, distinct_id=user_id,
groups={'company': company_id})
# Or once per request:
token = set_context_groups({'company': company_id}) # reset_context_groups(token) when done
The JavaScript AnalyticsClient holds one customer at a time. A Node.js server uses one
request-scoped client per
request, with its own identity and session. Name the request's account before capturing its events.
Verify identity
Ask your agent to find the customer by ID (search_persons over MCP, or anectico persons search <query>) and to read the timeline (get_person_timeline, or anectico persons timeline <customer-id>). Confirm that the timeline contains activity from the expected browser, mobile
application, and backend services. The MCP results carry a link that opens the customer as a read-only
proof page. If signals are present elsewhere but missing from the
timeline, follow Customer activity is not connecting.
Inspect and correct links
Owners and administrators can preview and separate an incorrectly linked alias from a customer profile. In the Console, open Privacy (/privacy) and use Identity links on the Requests tab. Your agent can do the same with preview_identity_correction and correct_identity. See Correct an identity link for the effects, permissions, history and limits.
Correct account membership
A group call adds a person to an account. To correct a membership that is wrong, for example after a
person changed companies, use the CLI or an MCP agent rather than sending more events:
anectico accounts members list company acme --include-inactive
anectico accounts members move company acme <distinct-id> --to globex
anectico accounts members remove company acme <distinct-id>
anectico accounts delete company acme --yes
- A move names the exact account the person leaves and the one they join, of the same type. A person who is not an active member of the source account is refused, never quietly added to the destination.
- A removed or moved membership is kept as history.
- Repeating a completed move or removal changes nothing.
- Deleting an account is refused while it still has active members. It is never a way to move people.
- These are memberships of your customers' accounts, not of your Anectico organization.
- MCP agents use
list_account_membersand the confirmed write actionsmove_account_member,remove_account_memberanddelete_account.
Account and operation context at the time of activity
A person's account membership and the account they are working in are different facts. Use activity context when a person can switch accounts or work in several accounts at once. It accompanies the relevant spans, logs and product events.
| Field | Meaning |
|---|---|
anectico.account.type and anectico.account.key |
A paired account identifier, such as company and acme. |
anectico.operation.id |
An opaque ID for one logical workflow, kept across its retries. |
anectico.attempt.id |
An optional ID for one attempt; requires an operation ID. |
Use identifiers without personal details or secrets. Values must be nonempty, no more than 255 UTF-8 bytes, with no control characters or surrounding whitespace. These fields describe customer activity; they never grant access to an organization or project. Existing content controls still apply.
JavaScript (after initNode installs its context manager):
import { context } from '@opentelemetry/api';
import { contextWithActivity } from '@anectico/sdk';
const activity = contextWithActivity(context.active(), {
accountType: 'company', accountKey: 'acme',
operationId: 'order-123', attemptId: 'attempt-2',
});
await context.with(activity, async () => {
await completeCheckout(); // instrumented spans/logs and captured events inherit context
});
Go: ContextWithActivity(ctx, ActivityContext{AccountType: "company", AccountKey: "acme", OperationID: "order-123", AttemptID: "attempt-2"}) returns a new context
and an error. Pass that context to tracing, logging, capture and outbound work.
Python: from anectico.activity import activity_scope, then
with activity_scope(account_type="company", account_key="acme", operation_id="order-123", attempt_id="attempt-2"): around the instrumented work.
The JavaScript, Go and Python helpers replace the full activity scope: omitted fields are cleared. They preserve the outer scope on exit and use W3C baggage for propagation through instrumented calls. Explicit event properties can override the inherited values. For queues, inject context into the job's carrier and extract it when consuming; do not store a mutable global current account. Other SDKs can send these attributes explicitly; they do not yet offer these activity-scope helpers.
Register the account using the existing SDK group(type, key, properties) flow
before reading its activity timeline. Activity attributes do not create account records
or restore deleted accounts. A person does not need membership in that account
for their explicitly stamped activity to appear on its timeline.
The account timeline prefers explicit activity context over membership history. If it is absent, existing capture-time event groups and membership intervals remain fallbacks. A grouped trace uses its one explicitly observed account even when its root has no account stamp. Conflicting account stamps in one trace are treated as ambiguous and excluded from account attribution; the full trace remains on the person timeline. Session and agent-run cards still use membership attribution. Account membership counts and account blast-radius summaries retain their existing membership meaning; they are not counts of explicitly stamped operations.
Account activity discovery follows the selected timeline range and your readable retention. Selecting an older readable range still finds explicitly stamped activity; there is no separate seven-day account-discovery limit. Membership assertions made before that range remain available to attribute activity within it.
Membership moves and removals preview the complete affected declared edges and their versions. Review the current and proposed edges before approving, then repeat identical arguments with the returned confirmation token. Changed membership refuses application. History remains.
Account type and key are exact values, including surrounding spaces or dot-only
keys. Detail and member CLI commands preserve them; quote them in the shell.
anectico accounts types reads the selected project. Account timelines accept
--since, --until and --signal; use fixed UTC bounds and keep all selections
when continuing with --cursor. Search without a query lists recent accounts;
account-type hints appear only when no accounts match.
Permanent removal of technical account associations
For permanent removal rather than a membership correction, follow Erase an account.
That page defines the affected evidence, verification states, late-delivery boundaries and external
copies. Read the erasure operation through MCP or anectico accounts erasures get; a local SDK
reset or a membership removal is not erasure.
Account stamps for observed request reliability
For each inbound request, set both anectico.account.type and anectico.account.key
on the server span from the authenticated application session. Replace inherited
account context when the request serves a different account, and clear it when no
account is served. Do not trust an incoming baggage value as account authorization.
The measuring span's own pair attributes the request. Current membership, another span's account and resource-level context cannot complete a missing pair. Requests without a resolved person can still contribute to the account they explicitly name. Preserve exact case, spaces and punctuation when selecting an account.
Keep identity and paired account context request-scoped, including concurrent requests and queued work. An account switch must replace the pair before the next measuring span starts; an anonymous or account-free request must clear inherited values. A server span can have a parent and still count; its own stamp is what matters. Membership changes and product-event groups are separate records, not a source for missing request stamps.
Person aliases must resolve before their attempts enter a canonical ranking. Missing and unresolved declarations stay explicit; identity projection refresh can lag changes by 30–60 seconds. Register the account through the owning account flow before reading it. An unresolved person can still contribute to a live explicitly stamped account. These reads disclose current identity and owner boundaries, not a frozen cross-source snapshot.
See observed customer request reliability for request-attempt outcomes, latency samples, account-stamp coverage and the limits on claims about complete application traffic.