Skip to content
Console
Browse documentation
Guide

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.

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_members and the confirmed write actions move_account_member, remove_account_member and delete_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.