Relevant source files
The following files were used as context for generating this wiki page:
- cli/node/src/telemetry.ts
- integrations/mem0-agent-plugin/core/telemetry.py
- mem0-ts/src/client/telemetry.ts
- mem0-ts/src/oss/src/memory/index.ts
- integrations/kimi-plugin/core/telemetry.py
- server/telemetry.py
- integrations/antigravity-plugin/core/telemetry.py
- cli/python/src/mem0_cli/telemetry.py
- integrations/agent-plugin-core/python/telemetry.py
- integrations/antigravity-plugin/core/memory_core.py
- integrations/kimi-plugin/core/memory_core.py
- integrations/mem0-agent-plugin/core/memory_core.py
- integrations/cursor-plugin/core/telemetry.py
- integrations/agent-plugin-core/python/memory_core.py
- integrations/claude-code-plugin/core/memory_core.py
- integrations/codex-plugin/core/memory_core.py
- integrations/cursor-plugin/core/memory_core.py
- integrations/codex-plugin/core/telemetry.py
- mem0/memory/telemetry.py
- integrations/claude-code-plugin/core/telemetry.py
- integrations/openclaw/telemetry.ts
- mem0-ts/src/client/mem0.ts
- cli/python/src/mem0_cli/telemetry_sender.py
- integrations/mem0-strands/python/src/mem0_strands/telemetry.py
- integrations/opencode-plugin/telemetry.ts
- integrations/agent-plugin-core/typescript/src/telemetry.ts
- mem0-ts/src/client/config.ts
- integrations/pi-agent-plugin/src/telemetry.ts
- integrations/deepseek-plugin/src/telemetry.ts
- mem0-ts/src/oss/src/utils/telemetry.ts
Overview
Mem0 implements a comprehensive usage telemetry architecture across its Python core, TypeScript SDK, command-line interfaces, self-hosted server, and IDE plugins to track adoption and feature funnels via PostHog. This system handles event sampling, anonymous per-machine identifiers, authenticated account email resolution, secret redaction, and non-blocking sub-process or batched network dispatches. By default, telemetry is enabled across all runtime surfaces but can be opted out universally through the MEM0_TELEMETRY=false environment variable.
Sources: cli/node/src/telemetry.ts:2-8, integrations/mem0-agent-plugin/core/telemetry.py:2-18, mem0-ts/src/client/telemetry.ts:10-15, server/telemetry.py:1-11, mem0/memory/telemetry.py:14-32
Python Core Anonymous Telemetry
Client Lifecycle and Configuration
The Python core telemetry engine relies on PostHog as its underlying event collector, configured via constants defined in mem0/memory/telemetry.py. Telemetry activation is governed by the MEM0_TELEMETRY environment variable, which defaults to "True" and is coerced into a boolean value. If an invalid non-boolean value is supplied, a ValueError is raised immediately upon module import.
| Constant / Variable | Type | Default Value | Description |
|---|---|---|---|
MEM0_TELEMETRY | bool | True | Master toggle controlling whether telemetry events are captured. |
PROJECT_API_KEY | str | "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX" | PostHog project API token for the mem0 platform. |
HOST | str | "https://us.i.posthog.com" | PostHog ingest endpoint host URL. |
FEATURE_FLAGS_REQUEST_TIMEOUT_SECONDS | float | 0.5 | Timeout threshold for PostHog feature flag queries. |
Sources: mem0/memory/telemetry.py:14-23, mem0/memory/telemetry.py:83-88
The AnonymousTelemetry class manages the lifecycle of the PostHog client instance and resolves user identities through get_or_create_user_id(). When disabled, its internal attributes are set to None. The class exposes capture_event(), which enriches outgoing payloads with standard environment metadata including client source (python), package version (mem0.__version__), system platform, Python runtime version, OS release, and hardware machine architecture. A process-wide thread-safe lazy singleton pattern governed by _get_oss_telemetry() and secured via _oss_telemetry_lock ensures that open-source memory instances reuse a single client instance, which is safely torn down at process exit through an atexit hook.
Warning
Telemetry capture methods catch all exceptions internally and suppress them using a debug logger; failures in network transmission or event serialization never propagate to caller application logic.
Sources: mem0/memory/telemetry.py:76-121, mem0/memory/telemetry.py:145-184, mem0/memory/telemetry.py:192-197
Sampling Mechanics and Lifecycle Events
Open-source hot-path telemetry events are subject to probabilistic sampling to minimize ingestion volume, while critical lifecycle events bypass sampling entirely and always execute at a 100% rate. The sampling rate defaults to 0.1 (10%) and can be overridden via the MEM0_TELEMETRY_SAMPLE_RATE environment variable. The helper function _parse_sample_rate() parses incoming raw values, catches parsing exceptions, validates that the float falls within the closed interval [0.0, 1.0], and falls back to the default rate on invalid configurations without raising errors.
+-------------------------------------------------+
| Incoming PostHog Message |
+-------------------------------------------------+
|
v
Is event in _LIFECYCLE_EVENTS?
/ \
YES NO
/ \
v v
Bypass Sampling random.random() >= MEM0_SAMPLE_RATE?
(sample_rate = 1.0) / \
YES NO
/ \
v v
Drop Message Keep Message
(return None) (sample_rate = rate)
Sources: mem0/memory/telemetry.py:30-48, mem0/memory/telemetry.py:53-74
The _sampling_before_send() hook function acts as PostHog's filtering mechanism before dispatch. It inspects outgoing message dictionaries for their event identifiers against _LIFECYCLE_EVENTS.
_LIFECYCLE_EVENTS = frozenset(
{"mem0.init", "mem0.reset", "mem0._create_procedural_memory", "mem0.notice_displayed", "$identify"}
)Sources: mem0/memory/telemetry.py:53-55
For non-lifecycle events, it evaluates a random draw against MEM0_TELEMETRY_SAMPLE_RATE, dropping the payload if the condition holds. Surviving events are annotated with a sample_rate property (1.0 for lifecycle events, or the configured sample rate for hot-path events) to allow PostHog dashboards to accurately extrapolate total event volumes.
Note
The $identify event is explicitly included in _LIFECYCLE_EVENTS to ensure that PostHog person-merging operations are never lost to statistical sampling.
Sources: mem0/memory/telemetry.py:50-74
TypeScript Telemetry and Notice Dispatch
UnifiedTelemetry Client and Environment Flags
The TypeScript open-source package implements telemetry dispatch through the UnifiedTelemetry class, which conforms to the TelemetryClient interface. The client initializes with a project API key (POSTHOG_API_KEY) and a telemetry host endpoint (POSTHOG_HOST), defaulting to PostHog's US ingestion URL (https://us.i.posthog.com/i/v0/e/). Telemetry collection can be disabled globally by setting the MEM0_TELEMETRY environment variable to false. SDK versioning is resolved at build time via bundler defines (__MEM0_SDK_VERSION__), falling back to "dev" in unbundled environments like Jest test suites.
// __MEM0_SDK_VERSION__ is inlined by tsup/esbuild's `define` at build time from
// package.json. In unbundled environments (ts-jest, jest globalSetup) the
// identifier is not defined, so guard with typeof to fall back safely.
let version =
typeof __MEM0_SDK_VERSION__ !== "undefined" ? __MEM0_SDK_VERSION__ : "dev";
// Safely check for process.env in different environments
let MEM0_TELEMETRY = true;
try {
MEM0_TELEMETRY =
process?.env?.MEM0_TELEMETRY?.toLowerCase() === "false" ? false : true;
} catch (error) {}
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
const NOTICE_EVENT_NAME = "mem0.notice_displayed";
Sources: mem0-ts/src/oss/src/utils/telemetry.ts:7-22
The captureEvent method inspects the MEM0_TELEMETRY boolean flag before constructing payloads. Outgoing event properties include the resolved client version, an ISO timestamp, custom properties, the PostHog library identifier ($lib: "posthog-node"), and a $process_person_profile boolean flag that disables person profile creation for anonymous distinct IDs (anonymous or anonymous-supabase). Network requests are dispatched via the global fetch API using HTTP POST with JSON-serialized payloads, catching and logging any transmission errors to console.error without disrupting application execution.
Warning
Telemetry HTTP transmission errors and environment parsing failures are caught and suppressed inside captureEvent and helper initializers; network failures or invalid environment values never throw exceptions up to the calling memory instance.
Sources: mem0-ts/src/oss/src/utils/telemetry.ts:13-18, mem0-ts/src/oss/src/utils/telemetry.ts:55-91
Sampling Mechanics and Event Dispatch
Hot-path event dispatch is governed by a probabilistic sampling rate defaulting to 0.1 (10%), configurable via the MEM0_TELEMETRY_SAMPLE_RATE environment variable. The sampling rate parser validates that parsed values fall within the numeric range [0, 1]. Events included in ALWAYS_SEND_EVENTS (init, reset, and notice_displayed) bypass sampling checks entirely and execute at a 100% rate (sample_rate: 1.0).
| Configuration Constant | Default Value | Purpose |
|---|---|---|
POSTHOG_API_KEY | phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX | PostHog project write key |
POSTHOG_HOST | https://us.i.posthog.com/i/v0/e/ | Ingestion API endpoint |
DEFAULT_SAMPLE_RATE | 0.1 | Default sampling ratio for hot-path events |
NOTICE_EVENT_NAME | mem0.notice_displayed | Event identifier for notice telemetry |
ALWAYS_SEND_EVENTS | Set(['init', 'reset', 'notice_displayed']) | Events exempt from sampling |
Sources: mem0-ts/src/oss/src/utils/telemetry.ts:19-44
The captureClientEvent function validates that the provided instance object contains a telemetryId. If sampling drops the event, execution returns early; otherwise, it assembles TelemetryEventData including constructor name, method name, API host, timestamp, client version, client source ("nodejs"), and additional properties, appending the sample_rate property last so callers cannot override it.
async function captureClientEvent(
eventName: string,
instance: TelemetryInstance,
additionalData: Record<string, any> = {},
) {
if (!instance.telemetryId) {
console.warn("No telemetry ID found for instance");
return;
}
// >= so that rate=0 drops everything and rate=1 keeps everything (Math.random() ∈ [0, 1)).
const alwaysSend = ALWAYS_SEND_EVENTS.has(eventName);
if (!alwaysSend && Math.random() >= MEM0_TELEMETRY_SAMPLE_RATE) {
return;
}
const eventData: TelemetryEventData = {
function: `${instance.constructor.name}`,
method: eventName,
api_host: instance.host,
timestamp: new Date().toISOString(),
client_version: version,
client_source: "nodejs",
...additionalData,
// sample_rate set AFTER the spread so callers can never override it
sample_rate: alwaysSend ? 1.0 : MEM0_TELEMETRY_SAMPLE_RATE,
};
await telemetry.captureEvent(
instance.telemetryId,
`mem0.${eventName}`,
eventData,
);
}Sources: mem0-ts/src/oss/src/utils/telemetry.ts:104-137
The dedicated captureNoticeEvent function handles notice display telemetry independently. It bypasses sample rate evaluation and transmits the mem0.notice_displayed event with sample_rate: 1.0 whenever an instance with a valid telemetryId displays a notice.
Tip
The sample_rate property is spread into eventData after ...additionalData or ...properties to guarantee that callers cannot accidentally or intentionally override the rate assigned by the SDK sampling logic.
Sources: mem0-ts/src/oss/src/utils/telemetry.ts:128-130, mem0-ts/src/oss/src/utils/telemetry.ts:139-161
CLI Telemetry and Subprocess Architecture
CLI Telemetry Recording and Anonymous ID Resolution
Both the Node.js and Python CLI packages implement anonymous usage tracking via PostHog through non-blocking, fire-and-forget mechanisms. Telemetry can be disabled by setting the MEM0_TELEMETRY environment variable to false, 0, or no. When enabled, captureEvent evaluates distinct identifiers using a strict priority order: a cached user email retrieved from /v1/ping/, an MD5 hash of the configured API key, or a persistent per-machine anonymous identifier.
function getDistinctId(): string {
try {
const config = loadConfig();
if (config.platform.userEmail) {
return config.platform.userEmail;
}
if (config.platform.apiKey) {
return createHash("md5").update(config.platform.apiKey).digest("hex");
}
} catch {
/* ignore */
}
try {
return getOrCreateAnonymousId();
} catch {
return `cli-anon-${randomUUID().replace(/-/g, "")}`;
}
}Sources: cli/node/src/telemetry.ts:61-78, cli/python/src/mem0_cli/telemetry.py:51-71
Persistent anonymous IDs are generated as random UUID-based strings prefixed with cli-anon- and stored locally in ~/.mem0/config.json under telemetry.anonymous_id (or telemetry.anonymous_id in Python). This guarantees that subsequent CLI executions on the same host share a unified PostHog identity rather than falling back to ephemeral strings. When a previously anonymous session resolves to a real user email, the SDK detects the transition away from the cli-anon- prefix and schedules a one-shot $identify event to link historical anonymous logs to the authenticated profile.
| Configuration Field | Source Path / Storage | Fallback Priority |
|---|---|---|
user_email | config.platform.userEmail / user_email | Priority 1 (cached from /v1/ping/) |
api_key | config.platform.apiKey / api_key | Priority 2 (MD5 hash digest) |
anonymous_id | config.telemetry.anonymousId / anonymous_id | Priority 3 (persistent machine UUID) |
Sources: cli/node/src/telemetry.ts:39-78, cli/python/src/mem0_cli/telemetry.py:31-71
Detached Subprocess Dispatch Architecture
To prevent network operations or email resolution delays from blocking CLI command execution, telemetry events are dispatched via detached background subprocesses (telemetry-sender.cjs in Node.js and mem0_cli.telemetry_sender in Python). The parent CLI process serializes the execution context into JSON and writes it directly to the child process's standard input stream before unreferencing and exiting immediately.
Sources: cli/node/src/telemetry.ts:88-157, cli/python/src/mem0_cli/telemetry.py:73-155, cli/python/src/mem0_cli/telemetry_sender.py:24-50
The standalone sender script reads the serialized payload from sys.stdin (falling back to sys.argv[1] for legacy compatibility), validates whether email resolution is required via /v1/ping/, and executes HTTP POST requests against PostHog endpoints using native URL handling (urllib.request in Python or fetch/spawn bindings).
Warning
All runtime exceptions, network timeouts, and persistence failures occurring inside the detached subprocess or parent spawn routine are silently caught and swallowed to guarantee that telemetry errors never disrupt CLI user experiences.
Sources: cli/node/src/telemetry.ts:154-156, cli/python/src/mem0_cli/telemetry.py:154-155, cli/python/src/mem0_cli/telemetry_sender.py:83-84
Server Lifecycle Telemetry and State
Server Installation State and Client Initialization
The self-hosted FastAPI application implements server-side anonymous telemetry via server/telemetry.py. Telemetry is enabled by default, matching the behavior of the open-source library, but operators can opt out by setting MEM0_TELEMETRY=false. Persistence of installation state is handled through a JSON file stored at /app/history/telemetry.json (configurable via MEM0_TELEMETRY_STATE_PATH).
State persistence and client access are managed thread-safely using a global Lock. When loading or saving state, file and JSON decoding exceptions are caught and logged to prevent application crashes. The PostHog client is lazily initialized with a project API key (phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX), a host endpoint (https://us.i.posthog.com), and geoIP disabling enabled. If the posthog package is missing from the environment, a warning is logged and client instantiation returns None.
PROJECT_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
HOST = "https://us.i.posthog.com"
ENABLED = os.environ.get("MEM0_TELEMETRY", "true").lower() not in {"0", "false", "no", "off"}
STATE_PATH = Path(os.environ.get("MEM0_TELEMETRY_STATE_PATH", "/app/history/telemetry.json"))
_lock = Lock()
_client: Any = None
_dashboard_nudge_logged = False
def _load_state() -> dict[str, Any]:
if not STATE_PATH.exists():
return {}
try:
return json.loads(STATE_PATH.read_text())
except (OSError, json.JSONDecodeError):
return {}
def _save_state(state: dict[str, Any]) -> None:
try:
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
STATE_PATH.write_text(json.dumps(state))
except OSError:
logging.exception("telemetry: failed to persist state")
def _install_id(state: dict[str, Any]) -> str:
install_id = state.get("install_id")
if not install_id:
install_id = str(uuid.uuid4())
state["install_id"] = install_id
_save_state(state)
return install_id
def _get_client():
global _client
if _client is not None:
return _client
try:
from posthog import Posthog
except ImportError:
logging.warning("telemetry: posthog package not installed; disabling")
return None
_client = Posthog(project_api_key=PROJECT_API_KEY, host=HOST, disable_geoip=True)
return _clientSources: server/telemetry.py:24-71
Note
The server telemetry module logs an informational status message at startup when telemetry is enabled, explicitly reminding operators that setting MEM0_TELEMETRY=false disables event transmission.
Sources: server/telemetry.py:74-77
Bounded Once-Per-Install Telemetry Delivery
Server lifecycle telemetry enforces a strict ceiling of at most two events per installation by tracking state keys (admin_registered_sent_at and onboarding_sent_at) inside the persistent telemetry state file.
| Event Name | Trigger Condition | Associated State Key | Extra Properties |
|---|---|---|---|
admin_registered | First admin account created via wizard or API | admin_registered_sent_at | email_domain, server_version |
onboarding_completed | Setup wizard reaches its final success state | onboarding_sent_at | email_domain, server_version, use_case |
def _capture_once(email: str, event: str, state_key: str, extra: dict[str, Any] | None = None) -> None:
if not ENABLED:
return
with _lock:
state = _load_state()
if state.get(state_key):
return
client = _get_client()
if client is None:
return
try:
client.capture(
distinct_id=_install_id(state),
event=event,
properties={
"email_domain": email.rsplit("@", 1)[-1].lower() if "@" in email else "unknown",
"server_version": mem0.__version__,
**(extra or {}),
},
)
state[state_key] = datetime.now(timezone.utc).isoformat()
_save_state(state)
except Exception:
logging.exception("telemetry: failed to send %s event", event)
def capture_admin_registered(email: str) -> None:
_capture_once(email, "admin_registered", "admin_registered_sent_at")
def capture_onboarding_completed(email: str, use_case: str) -> None:
_capture_once(email, "onboarding_completed", "onboarding_sent_at", {"use_case": use_case})
Sources: server/telemetry.py:79-114
Caution
Dashboard installs emit both events (admin_registered followed by onboarding_completed), whereas API-only installs emit only the admin_registered event because they bypass the setup wizard.
Sources: server/telemetry.py:1-5
Events share common properties including the extracted email_domain (parsed by splitting the operator's email at the @ symbol), the current server_version from mem0.__version__, and a randomly generated install UUID (install_id). The onboarding_completed event additionally carries the operator's freeform use_case string.
Separately, the module provides log_dashboard_nudge_once, which logs a local console hint directing operators to the web dashboard upon storing their first memory. This nudge fires at most once per process and is intentionally un-gated by MEM0_TELEMETRY since it never sends data off-box.
def log_dashboard_nudge_once(dashboard_url: str) -> None:
"""Log a hint pointing the operator to the web dashboard the first time a memory
is stored. Fires at most once per process (so a restart may re-log it on the next
add — harmless for a discovery nudge). LOCAL console log only — sends nothing
off-box, so it is intentionally NOT gated by MEM0_TELEMETRY.
"""
global _dashboard_nudge_logged
if _dashboard_nudge_logged:
return
_dashboard_nudge_logged = True
logging.info(
"First memory stored. Open the dashboard at %s to view and manage your memories.",
dashboard_url,
)Sources: server/telemetry.py:116-129
Python Agent Plugin Spool System
File-Based Spool Claiming and Mechanics
Agent plugins operate under strict time budgets per tool call, requiring telemetry recording to avoid network access entirely. The recording method appends individual JSON lines to a local spool file without blocking execution, while a detached background process drains the spool into batched PostHog requests. To prevent concurrent senders from duplicating transmissions or interleaving flushes, the spool system uses an atomic file-based claiming mechanism governed by strict time limits and attempt counters.
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
POSTHOG_CAPTURE_URL = "https://us.i.posthog.com/i/v0/e/"
POSTHOG_BATCH_URL = "https://us.i.posthog.com/batch/"
EVENT_PREFIX = "code"
SPOOL_LIMIT_BYTES = 256 * 1024
BATCH_SIZE = 100
SEND_TIMEOUT = 5
CLAIM_STALE_SECONDS = 120
CLAIM_EXPIRY_SECONDS = 7 * 24 * 60 * 60
# A batch is only discarded once it has genuinely been retried this many times.
MAX_CLAIM_ATTEMPTS = 3
# Parked claims drained per run, after the live spool. Bounded so a long backlog
# cannot turn one flush into an unbounded send loop.
MAX_PARKED_PER_RUN = 3
# Added to the wait before a released claim becomes reclaimable, per attempt
# already spent. Releasing straight to "reclaimable now" let two senders burn the
# whole budget within seconds of one another on a single momentary failure, and
# discard a batch a retry a minute later would have delivered.
RETRY_COOLDOWN_SECONDS = 60
Sources: integrations/mem0-agent-plugin/core/telemetry.py:94-113
| Constant Name | Value | Meaning |
|---|---|---|
SPOOL_LIMIT_BYTES | 262144 (256 KB) | Maximum size threshold for the local telemetry spool file |
BATCH_SIZE | 100 | Maximum number of events transmitted in a single PostHog batch |
SEND_TIMEOUT | 5 | Network request timeout in seconds for PostHog API calls |
CLAIM_STALE_SECONDS | 120 | Duration after which an unreleased claim is considered stale |
CLAIM_EXPIRY_SECONDS | 604800 (7 days) | Absolute time limit before a parked claim expires permanently |
MAX_CLAIM_ATTEMPTS | 3 | Maximum number of delivery attempts before a batch is discarded |
MAX_PARKED_PER_RUN | 3 | Bounded limit on parked claims drained per flush run |
RETRY_COOLDOWN_SECONDS | 60 | Cooldown wait time added per prior attempt before a released claim re-enters service |
SPOOL_LIMIT_BYTES = 256 * 1024
BATCH_SIZE = 100
SEND_TIMEOUT = 5
CLAIM_STALE_SECONDS = 120
CLAIM_EXPIRY_SECONDS = 7 * 24 * 60 * 60
# A batch is only discarded once it has genuinely been retried this many times.
MAX_CLAIM_ATTEMPTS = 3
# Parked claims drained per run, after the live spool. Bounded so a long backlog
# cannot turn one flush into an unbounded send loop.
MAX_PARKED_PER_RUN = 3
# Added to the wait before a released claim becomes reclaimable, per attempt
# already spent. Releasing straight to "reclaimable now" let two senders burn the
# whole budget within seconds of one another on a single momentary failure, and
# discard a batch a retry a minute later would have delivered.
RETRY_COOLDOWN_SECONDS = 60Sources: integrations/mem0-agent-plugin/core/telemetry.py:98-112
Caution
Releasing failed claims directly to immediate availability would allow concurrent senders to exhaust the entire timeout budget within seconds on a momentary network failure; scaling the cooldown by attempts already spent ensures resilient retries.
Sources: integrations/mem0-agent-plugin/core/telemetry.py:108-112
Install State Repair and Identity Rotation
Plugin telemetry maintains identity separation by storing the per-install salt in a dedicated file rather than embedding it inside shared identity dictionaries. This prevents race conditions across short-lived concurrent agent windows and ensures that background read-modify-write cycles do not overwrite separate property keys like user email addresses or repository hashes.
def _install_salt() -> str:
"""Random per-install salt, created once and memoized for the process.
Deliberately its own file, claimed with O_CREAT|O_EXCL, rather than a key in
the identity file. Three reasons, all of which produced wrong data when this
lived in the identity dict:
- Hooks are short-lived separate processes firing on every tool call, and
people run more than one agent window. A read-modify-write would let each
process mint its own salt, so one repository would hash several ways in the
window before a writer won.
- resolve_distinct_id holds a copy of the identity dict across a network call
to /v1/ping/, so whichever write landed second erased the other's key —
losing either the salt (repo_hash changes mid-stream) or the email (a
second $identify, splitting the person).
- Touching the identity file from record() would create it, and is_first_run
keys off that file, so recording an event would silently suppress the
install event.
Published atomically, and there is deliberately no derived fallback. Creating
the file with O_CREAT|O_EXCL and then writing into it leaves a window where
the file exists and is empty, and a concurrent hook that reads it in that
window gets nothing. Falling back to a digest of the path would hand that
process a salt an attacker can compute, memoized for its whole run, which is
the privacy control this function exists to provide silently turning itself
off under load. The salt is written to a private temp file first and linked
into place, so the name either does not exist or already has the full value.
Returns "" when it genuinely cannot persist. Callers omit the hash entirely
rather than emit an unsalted one.
"""Sources: integrations/mem0-agent-plugin/core/telemetry.py:134-164
Warning
Creating salt files directly with O_CREAT|O_EXCL and writing inline exposes a vulnerable window where concurrent hooks reading an empty file receive nothing; salts must be written to a private temporary file first and atomically linked into position.
Sources: integrations/mem0-agent-plugin/core/telemetry.py:153-161
The initialization module supports explicit harness overrides via init(), harmonizing source tags across disparate processes. The fallback naming convention enforces standard <HOST>_PLUGIN tags across MCP servers and detached flush senders alike.
def init(harness: str = "", source_tag: str = "") -> None:
"""Override the generated identity. Optional — core/_harness_id.py is the default.
The fallback shape matches memory_core.configure_harness's (``<HOST>_PLUGIN``).
It used to be ``MEM0_<HOST>_PLUGIN`` here and ``<host>_plugin`` there, which
meant one plugin could emit three different source values depending on which
process happened to send the batch.
"""
global _harness, _source_tag
_harness = harness or _DEFAULT_HARNESS
_source_tag = source_tag or (
f"{_harness.upper().replace('-', '_')}_PLUGIN" if harness else _DEFAULT_SOURCE_TAG
)Sources: integrations/mem0-agent-plugin/core/telemetry.py:80-92
TypeScript Plugin Privacy and Identity
TypeScript Agent Plugin Telemetry Client
The TypeScript agent plugin architecture centers on a unified telemetry client implemented in integrations/agent-plugin-core/typescript/src/telemetry.ts and specialized across runner integrations like OpenClaw, OpenCode, Pi Agent, and DeepSeek. Telemetry configuration accepts a TelemetryConfig interface defining distinct identity resolvers, event name translators, delivery handlers, and enablement check functions.
export interface TelemetryConfig {
host: string;
source: string;
version: string;
distinctId: string | (() => string | undefined);
delivery?: (batch: Record<string, unknown>[]) => void | Promise<void>;
flushThreshold?: number;
flushIntervalMs?: number;
maxQueueSize?: number;
commonProperties?: Record<string, unknown>;
eventName?: (event: string) => string;
enabled?: () => boolean;
}Sources: integrations/agent-plugin-core/typescript/src/telemetry.ts:32-44
Events are processed through a bounded in-memory queue backed by automatic flushing thresholds, a configurable maximum queue size of 100 entries, and a delivery retry mechanism capped at 5 attempts with exponential backoff.
export function createTelemetry(config: TelemetryConfig) {
let queue: Record<string, unknown>[] = [];
let timer: ReturnType<typeof setInterval> | undefined;
let consecutiveFailures = 0;
let retryNotBefore = 0;
let exitFlushAttempted = false;
let flushing = false;
const flushThreshold = config.flushThreshold ?? 10;
const maxQueueSize = config.maxQueueSize ?? 100;
Sources: integrations/agent-plugin-core/typescript/src/telemetry.ts:98-107
| Configuration Option | Default Value / Behavior | Purpose |
|---|---|---|
flushThreshold | 10 | Event count required to trigger an immediate batch flush |
maxQueueSize | 100 | Maximum retention boundary; drops new events on overflow rather than evicting the retry backlog |
flushIntervalMs | 5000 | Periodic timer interval in milliseconds for background queue flushing |
POSTHOG_API_KEY | phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX | Hardcoded PostHog project API key used across plugin batches |
POSTHOG_BATCH_URL | https://us.i.posthog.com/batch/ | Endpoint URL receiving batched telemetry payloads |
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_BATCH_URL = "https://us.i.posthog.com/batch/";Sources: integrations/agent-plugin-core/typescript/src/telemetry.ts:5-6, integrations/agent-plugin-core/typescript/src/telemetry.ts:105-106
Warning
Telemetry delivery is retried in memory rather than spooled to disk; a session that starts and ends without network connectivity will lose its queued telemetry, avoiding complex cross-process file lease machinery.
Sources: integrations/agent-plugin-core/typescript/src/telemetry.ts:79-85
Secret Redaction and Key Filtering
To protect user confidentiality, property values and object payloads traverse a recursive sanitization pipeline before event construction. The safeValue utility function intercepts strings to apply redactSecrets, handles arrays recursively, and filters object keys against a comprehensive set of sensitive identifiers.
function safeValue(value: unknown): unknown {
if (typeof value === "string") return redactSecrets(value);
if (Array.isArray(value)) return value.map(safeValue);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value)
.filter(([key]) => !PRIVATE_KEYS.has(key.toLowerCase().replace(/[^a-z]/g, "")))
.map(([key, nested]) => [key, safeValue(nested)]),
);
}
return value;
}Sources: integrations/agent-plugin-core/typescript/src/telemetry.ts:51-62
The PRIVATE_KEYS set enumerates field names that are scrubbed from telemetry property dictionaries, normalized by lowercasing and stripping non-alphabetic characters.
| Blocklisted Property Key | Blocklisted Property Key | Blocklisted Property Key |
|---|---|---|
apikey | authorization | password |
query | secret | prompt |
token | text | memory |
message | error | path |
cwd | userid | agentid |
runid | repoid | repositoryid |
projectid | appid | filters |
const PRIVATE_KEYS = new Set([
"apikey",
"authorization",
"password",
"query",
"secret",
"prompt",
"token",
"text",
"memory",
"message",
"error",
"path",
"cwd",
"userid",
"agentid",
"runid",
"repoid",
"repositoryid",
"projectid",
"appid",
"filters",
]);Sources: integrations/agent-plugin-core/typescript/src/telemetry.ts:8-30
Note
Key filtering normalizes property keys by removing all non-alphabetic characters and converting to lowercase before checking membership in PRIVATE_KEYS, ensuring variants like apiKey or repo_id are caught.
Sources: integrations/agent-plugin-core/typescript/src/telemetry.ts:57-57
Key Hashing and Distinct ID Resolution
Different runner integrations implement tailored distinct ID resolution strategies to balance anonymity with account continuity. The OpenClaw plugin supports asynchronous email resolution by pinging /v1/ping/ with an API key token header, computing SHA-256 key fingerprints, and updating queued events when user identity is confirmed.
function keyFingerprint(apiKey?: string): string {
return apiKey ? createHash("sha256").update(apiKey).digest("hex").slice(0, 16) : "";
}
function distinctId(apiKey?: string): string {
try {
const auth = readPluginAuth();
if (auth.userEmail) {
// Only when it belongs to the key in hand. Without this check a cached
// email was used forever: switch to a different account and every event
// kept reporting under the previous one, with nothing to notice it by.
if (auth.keyFingerprint === keyFingerprint(apiKey)) {
return createHash("sha256").update(auth.userEmail).digest("hex");
}
// Only a REAL key that disagrees means the account changed. Without the
// apiKey guard the comparison is `undefined === ""` for any call that
// simply omits the key, so a capture with no context wiped a perfectly
// good account out of openclaw.json.
//
// A row with an email and NO fingerprint is the legacy shape, from an
// install predating this field. Clearing it here deleted a real account
// before anything had replaced it, and if the re-resolve then failed
// because the user was offline the email was gone from disk for good. The
// Python core refuses the same trade: verify, and keep what you have until
// the verification succeeds. resolveEmail below overwrites both fields
// when it does, so there is nothing to clear first.
if (apiKey && auth.keyFingerprint) clearResolvedAccount();
}
} catch {
// Fall through to the API key or anonymous identity.
}
return apiKey ? createHash("sha256").update(apiKey).digest("hex") : anonymousId();
}Sources: integrations/openclaw/telemetry.ts:43-75, integrations/openclaw/telemetry.ts:99-129
By contrast, the OpenCode plugin hashes API keys to derive 32-character distinct identifiers and salts project identifiers with the API key to prevent enumeration attacks.
function distinctId(apiKey: string): string {
return createHash("sha256").update(apiKey).digest("hex").slice(0, 32);
}
/**
* Salted so the hash is not enumerable.
*
* An unsalted SHA-256 of a project id is reversible by anyone who can guess the
* id, which for a project identifier is a small space. The API key is the salt:
* it is already in play here (distinctId is a digest of it), it is high entropy,
* and using it needs no per-install file and so no write race to get wrong. The
* hash is therefore per account rather than per machine, which also keeps joins
* working for one user across machines. It resets when the key rotates, which is
* consistent, because distinctId resets with it.
*
* Both are omitted without a key. An event cannot be built without a distinctId
* anyway, so this costs nothing.
*/
function projectHash(projectId?: string, apiKey?: string): Record<string, string> {
if (!projectId || !apiKey) return {};
return { project_hash: createHash("sha256").update(`${apiKey}:${projectId}`).digest("hex") };
}Sources: integrations/opencode-plugin/telemetry.ts:41-62
Pi Agent and DeepSeek plugins initialize anonymous IDs stored in local configuration files under ~/.mem0/ or plugin config directories, issuing $identify events when an API key eventually supersedes the anonymous session identifier.
function anonymousId(): string {
if (cachedAnonymousId) return cachedAnonymousId;
try {
const stored = JSON.parse(fs.readFileSync(TELEMETRY_ID_PATH, "utf-8")).anonymousId;
if (typeof stored === "string" && stored) return (cachedAnonymousId = stored);
} catch {
// First run or unreadable identity file.
}
const created = `pi-mem0-anon-${randomUUID().replace(/-/g, "")}`;
try {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(TELEMETRY_ID_PATH, JSON.stringify({ anonymousId: created }), "utf-8");
} catch {
// An unwritable config directory must not break the plugin.
}
return (cachedAnonymousId = created);
}
function distinctId(apiKey?: string): string {
return apiKey ? createHash("sha256").update(apiKey).digest("hex") : anonymousId();
}
function previousAnonymousId(id: string): string | undefined {
if (identified || id.startsWith("pi-mem0-anon-")) return undefined;
identified = true;
try {
const stored = JSON.parse(fs.readFileSync(TELEMETRY_ID_PATH, "utf-8")).anonymousId;
fs.unlinkSync(TELEMETRY_ID_PATH);
cachedAnonymousId = undefined;
return typeof stored === "string" && stored ? stored : undefined;
} catch {
return undefined;
}
}Sources: integrations/pi-agent-plugin/src/telemetry.ts:21-54, integrations/deepseek-plugin/src/telemetry.ts:34-65
Tip
OpenClaw latches email resolution attempts per key fingerprint rather than globally per process, allowing mid-session API key rotations to correctly trigger re-authentication checks.
Sources: integrations/openclaw/telemetry.ts:99-104
Related
On This Page
OverviewPython Core Anonymous TelemetryClient Lifecycle and ConfigurationSampling Mechanics and Lifecycle EventsTypeScript Telemetry and Notice DispatchUnifiedTelemetry Client and Environment FlagsSampling Mechanics and Event DispatchCLI Telemetry and Subprocess ArchitectureCLI Telemetry Recording and Anonymous ID ResolutionDetached Subprocess Dispatch ArchitectureServer Lifecycle Telemetry and StateServer Installation State and Client InitializationBounded Once-Per-Install Telemetry DeliveryPython Agent Plugin Spool SystemFile-Based Spool Claiming and MechanicsInstall State Repair and Identity RotationTypeScript Plugin Privacy and IdentityTypeScript Agent Plugin Telemetry ClientSecret Redaction and Key FilteringKey Hashing and Distinct ID ResolutionRelated