Skip to main content
doc0blade47/mem0documentation home
Sign in

Telemetry and Metrics

Last updated Oct 11, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

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 / VariableTypeDefault ValueDescription
MEM0_TELEMETRYboolTrueMaster toggle controlling whether telemetry events are captured.
PROJECT_API_KEYstr"phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"PostHog project API token for the mem0 platform.
HOSTstr"https://us.i.posthog.com"PostHog ingest endpoint host URL.
FEATURE_FLAGS_REQUEST_TIMEOUT_SECONDSfloat0.5Timeout 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 ConstantDefault ValuePurpose
POSTHOG_API_KEYphc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yXPostHog project write key
POSTHOG_HOSThttps://us.i.posthog.com/i/v0/e/Ingestion API endpoint
DEFAULT_SAMPLE_RATE0.1Default sampling ratio for hot-path events
NOTICE_EVENT_NAMEmem0.notice_displayedEvent identifier for notice telemetry
ALWAYS_SEND_EVENTSSet(['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 FieldSource Path / StorageFallback Priority
user_emailconfig.platform.userEmail / user_emailPriority 1 (cached from /v1/ping/)
api_keyconfig.platform.apiKey / api_keyPriority 2 (MD5 hash digest)
anonymous_idconfig.telemetry.anonymousId / anonymous_idPriority 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 _client

Sources: 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 NameTrigger ConditionAssociated State KeyExtra Properties
admin_registeredFirst admin account created via wizard or APIadmin_registered_sent_atemail_domain, server_version
onboarding_completedSetup wizard reaches its final success stateonboarding_sent_atemail_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 NameValueMeaning
SPOOL_LIMIT_BYTES262144 (256 KB)Maximum size threshold for the local telemetry spool file
BATCH_SIZE100Maximum number of events transmitted in a single PostHog batch
SEND_TIMEOUT5Network request timeout in seconds for PostHog API calls
CLAIM_STALE_SECONDS120Duration after which an unreleased claim is considered stale
CLAIM_EXPIRY_SECONDS604800 (7 days)Absolute time limit before a parked claim expires permanently
MAX_CLAIM_ATTEMPTS3Maximum number of delivery attempts before a batch is discarded
MAX_PARKED_PER_RUN3Bounded limit on parked claims drained per flush run
RETRY_COOLDOWN_SECONDS60Cooldown 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 = 60

Sources: 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 OptionDefault Value / BehaviorPurpose
flushThreshold10Event count required to trigger an immediate batch flush
maxQueueSize100Maximum retention boundary; drops new events on overflow rather than evicting the retry backlog
flushIntervalMs5000Periodic timer interval in milliseconds for background queue flushing
POSTHOG_API_KEYphc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yXHardcoded PostHog project API key used across plugin batches
POSTHOG_BATCH_URLhttps://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 KeyBlocklisted Property KeyBlocklisted Property Key
apikeyauthorizationpassword
querysecretprompt
tokentextmemory
messageerrorpath
cwduseridagentid
runidrepoidrepositoryid
projectidappidfilters
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

Was this page helpful?