# Identify customers

> Use one stable customer identity across errors, traces, logs, replay, events, and AI calls.

Canonical page: https://anectico.com/docs/instrument/identity/


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.

## 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.

```text
good: user_8842
avoid: buyer@acme.example
```

Email and name belong in customer properties. They remain searchable without becoming the primary
identity key.

## Identify after authentication

Call the SDK's identify method after your application knows who the customer is.

The API key needs `analytics:write` for identify and group calls. If the same SDK also sends traces,
logs, metrics, or errors, it needs `ingest:write` as well.

<div data-language-tabs="identity-identify" data-language-tabs-label="Choose an SDK"></div>

**JavaScript / TypeScript**

```typescript
anectico.identify('user_8842', {
  email: 'buyer@acme.example',
  plan: 'pro',
});
```

**Python**

```python
client.identify('user_8842', properties={
    'email': 'buyer@acme.example',
    'plan': 'pro',
})
```

**Go**

```go
if err := client.Identify(ctx, "user_8842", map[string]any{
	"email": "buyer@acme.example",
	"plan":  "pro",
}); err != nil {
	return err
}
```

**Swift (iOS)**

```swift
Anectico.identify("user_8842", set: [
    "email": "buyer@acme.example",
    "plan": "pro",
])
```

**Kotlin (Android)**

```kotlin
Anectico.identify(
    "user_8842",
    mapOf("email" to "buyer@acme.example", "plan" to "pro"),
)
```

**React Native**

```typescript
await Anectico.identify('user_8842', {
  email: 'buyer@acme.example',
  plan: 'pro',
});
```

**Flutter**

```dart
await Anectico.identify('user_8842', set: {
  'email': 'buyer@acme.example',
  'plan': 'pro',
});
```

<div data-language-tabs-end="identity-identify"></div>

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](/docs/reference/rest-api#direct-customer-property-updates).
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.

<div data-language-tabs="identity-reset" data-language-tabs-label="Choose an SDK"></div>

**JavaScript / TypeScript**

```typescript
anectico.reset();
```

**Python**

```python
client.reset()
```

**Go**

```go
if err := client.Reset(ctx); err != nil {
	return err
}
```

**Swift (iOS)**

```swift
Anectico.reset()
```

**Kotlin (Android)**

```kotlin
Anectico.reset()
```

**React Native**

```typescript
await Anectico.reset();
```

**Flutter**

```dart
await Anectico.reset();
```

<div data-language-tabs-end="identity-reset"></div>

Resetting establishes a new user boundary: it rotates the SDK's anonymous identity/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.

## 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.

<div data-language-tabs="identity-request" data-language-tabs-label="Choose a server SDK"></div>

**JavaScript / TypeScript**

```typescript
import { context } from '@opentelemetry/api';
import { contextWithDistinctId } from '@anectico/sdk';

const requestContext = contextWithDistinctId(context.active(), user.id);
await context.with(requestContext, () => handleRequest());
```

**Python**

```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**

```go
requestContext := anectico.ContextWithDistinctID(r.Context(), user.ID)
```

<div data-language-tabs-end="identity-request"></div>

The request-scoped identity should be active before creating spans, capturing errors, or writing
logs.

## 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.

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:

<div data-language-tabs="identity-group" data-language-tabs-label="Choose an SDK"></div>

**JavaScript / TypeScript**

```typescript
anectico.group('company', 'acme', { name: 'Acme Inc' });
```

**Python**

```python
client.group('company', 'acme', {'name': 'Acme Inc'})
```

**Go**

```go
if err := client.Group(ctx, "company", "acme", map[string]any{
	"name": "Acme Inc",
}); err != nil {
	return err
}
```

**Swift (iOS)**

```swift
Anectico.group(type: "company", key: "acme", set: ["name": "Acme Inc"])
```

**Kotlin (Android)**

```kotlin
Anectico.group("company", "acme", mapOf("name" to "Acme Inc"))
```

**React Native**

```typescript
await Anectico.group('company', 'acme', { name: 'Acme Inc' });
```

**Flutter**

```dart
await Anectico.group('company', 'acme', set: {'name': 'Acme Inc'});
```

<div data-language-tabs-end="identity-group"></div>

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.

## Verify identity

Open **Customers**, search by ID or email, and confirm that the timeline contains activity from the
expected browser, mobile application, and backend services. If signals are present elsewhere but
missing from the timeline, follow [Customer activity is not connecting](/docs/help/identity-not-linking).

- [Choose safe API-key scopes](/docs/reference/authentication)
- [Add diagnostic events](/docs/instrument/diagnostic-events)

## Inspect and correct links

Owners and administrators can preview and separate an incorrectly linked alias from a customer profile. See [Identity correction and instrumentation doctor](/docs/instrument/diagnostics) for the effects, permissions, history and limits.

## 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):

```typescript
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 opening its account page. 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.
