fix(openclaw): persistent anonymous telemetry ID, flush fix, and email resolution (#4790)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9d82e2329d
commit
d39ebad09f
@@ -4,6 +4,20 @@ description: "Release notes for the OpenClaw plugin and agent harness."
|
|||||||
mode: "wide"
|
mode: "wide"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<Update label="2026-04-11" description="v1.0.6">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Telemetry:** Replaced shared `"anonymous-openclaw"` fallback with a persistent per-machine random hash (`openclaw-anon-<uuid>`), so anonymous plugin users are counted individually in PostHog ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
- **Telemetry:** Added PostHog `$identify` event on first authenticated run to stitch anonymous history onto the authenticated profile ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
- **Telemetry:** Fixed event loss on short-lived CLI invocations — added `beforeExit` handler to flush queued events before the process exits ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
- **Telemetry:** Added lazy `/v1/ping/` email resolution so users who configure API key outside `mem0 init` show as their email in PostHog, not an md5 hash ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
- **Telemetry:** Unified CLI event prefix from `openclaw.<cmd>` to `openclaw.cli.<cmd>` on the needsSetup branch to match the authenticated branch ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **API:** Added `source: "OPENCLAW"` to all provider calls (`add`, `search`, `getAll`) across tools, CLI commands, recall, and the OSS backend adapter ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
<Update label="2026-04-07" description="v1.0.5">
|
<Update label="2026-04-07" description="v1.0.5">
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
|
|||||||
@@ -621,7 +621,6 @@ export function registerCliCommands(
|
|||||||
runId?: string,
|
runId?: string,
|
||||||
): SearchOptions => {
|
): SearchOptions => {
|
||||||
const base = buildSearchOptions(userIdOverride, lim, runId);
|
const base = buildSearchOptions(userIdOverride, lim, runId);
|
||||||
delete (base as any).source;
|
|
||||||
base.threshold = 0.3;
|
base.threshold = 0.3;
|
||||||
return base;
|
return base;
|
||||||
};
|
};
|
||||||
@@ -718,7 +717,7 @@ export function registerCliCommands(
|
|||||||
: effectiveUserId(getCurrentSessionId());
|
: effectiveUserId(getCurrentSessionId());
|
||||||
const result = await provider.add(
|
const result = await provider.add(
|
||||||
[{ role: "user", content: text }],
|
[{ role: "user", content: text }],
|
||||||
{ user_id: uid },
|
{ user_id: uid, source: "OPENCLAW" },
|
||||||
);
|
);
|
||||||
const count = result.results?.length ?? 0;
|
const count = result.results?.length ?? 0;
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface PluginAuthConfig {
|
|||||||
autoRecall?: boolean;
|
autoRecall?: boolean;
|
||||||
autoCapture?: boolean;
|
autoCapture?: boolean;
|
||||||
topK?: number;
|
topK?: number;
|
||||||
|
anonymousTelemetryId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -78,6 +79,7 @@ export function readPluginAuth(): PluginAuthConfig {
|
|||||||
autoRecall: cfg.autoRecall as boolean | undefined,
|
autoRecall: cfg.autoRecall as boolean | undefined,
|
||||||
autoCapture: cfg.autoCapture as boolean | undefined,
|
autoCapture: cfg.autoCapture as boolean | undefined,
|
||||||
topK: cfg.topK as number | undefined,
|
topK: cfg.topK as number | undefined,
|
||||||
|
anonymousTelemetryId: cfg.anonymousTelemetryId as string | undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ const memoryPlugin = definePluginEntry({
|
|||||||
(id: string) => `${cfg.userId}:agent:${id}`,
|
(id: string) => `${cfg.userId}:agent:${id}`,
|
||||||
() => ({ user_id: cfg.userId, top_k: cfg.topK }),
|
() => ({ user_id: cfg.userId, top_k: cfg.topK }),
|
||||||
() => undefined,
|
() => undefined,
|
||||||
(cmd: string) => _captureEvent(`openclaw.${cmd}`, { command: cmd }),
|
(cmd: string) => _captureEvent(`openclaw.cli.${cmd}`, { command: cmd }),
|
||||||
);
|
);
|
||||||
|
|
||||||
api.registerService({
|
api.registerService({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mem0/openclaw-mem0",
|
"name": "@mem0/openclaw-mem0",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -506,6 +506,7 @@ export function providerToBackend(
|
|||||||
msgs as Array<{ role: string; content: string }>,
|
msgs as Array<{ role: string; content: string }>,
|
||||||
{
|
{
|
||||||
user_id: opts.userId ?? userId,
|
user_id: opts.userId ?? userId,
|
||||||
|
source: "OPENCLAW",
|
||||||
...(opts.runId && { run_id: opts.runId }),
|
...(opts.runId && { run_id: opts.runId }),
|
||||||
...(opts.metadata && { metadata: opts.metadata }),
|
...(opts.metadata && { metadata: opts.metadata }),
|
||||||
...(opts.immutable && { immutable: true }),
|
...(opts.immutable && { immutable: true }),
|
||||||
@@ -524,6 +525,7 @@ export function providerToBackend(
|
|||||||
keyword_search: opts.keyword,
|
keyword_search: opts.keyword,
|
||||||
reranking: opts.rerank,
|
reranking: opts.rerank,
|
||||||
filters: opts.filters,
|
filters: opts.filters,
|
||||||
|
source: "OPENCLAW",
|
||||||
});
|
});
|
||||||
return results as unknown as Record<string, unknown>[];
|
return results as unknown as Record<string, unknown>[];
|
||||||
},
|
},
|
||||||
@@ -537,6 +539,7 @@ export function providerToBackend(
|
|||||||
const items = await provider.getAll({
|
const items = await provider.getAll({
|
||||||
user_id: opts.userId ?? userId,
|
user_id: opts.userId ?? userId,
|
||||||
page_size: opts.pageSize,
|
page_size: opts.pageSize,
|
||||||
|
source: "OPENCLAW",
|
||||||
});
|
});
|
||||||
return items as unknown as Record<string, unknown>[];
|
return items as unknown as Record<string, unknown>[];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ export async function recall(
|
|||||||
threshold,
|
threshold,
|
||||||
keyword_search: recallConfig.keywordSearch !== false, // Default on
|
keyword_search: recallConfig.keywordSearch !== false, // Default on
|
||||||
reranking: recallConfig.rerank !== false, // Default on
|
reranking: recallConfig.rerank !== false, // Default on
|
||||||
|
source: "OPENCLAW",
|
||||||
};
|
};
|
||||||
if (recallConfig.filterMemories) {
|
if (recallConfig.filterMemories) {
|
||||||
searchOpts.filter_memories = true;
|
searchOpts.filter_memories = true;
|
||||||
|
|||||||
+186
-5
@@ -8,10 +8,10 @@
|
|||||||
* Disable with: MEM0_TELEMETRY=false
|
* Disable with: MEM0_TELEMETRY=false
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createHash } from "node:crypto";
|
import { createHash, randomUUID } from "node:crypto";
|
||||||
import { readPluginAuth } from "./cli/config-file.ts";
|
import { readPluginAuth, writePluginAuth, getBaseUrl } from "./cli/config-file.ts";
|
||||||
|
|
||||||
export const PLUGIN_VERSION = "1.0.4";
|
export const PLUGIN_VERSION = "1.0.6";
|
||||||
|
|
||||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||||
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
|
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
|
||||||
@@ -22,6 +22,133 @@ const FLUSH_THRESHOLD = 10;
|
|||||||
let eventQueue: Record<string, unknown>[] = [];
|
let eventQueue: Record<string, unknown>[] = [];
|
||||||
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
|
|
||||||
|
let _cachedAnonymousId: string | undefined;
|
||||||
|
let _aliasCheckDone = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a persistent per-machine anonymous ID, generating one if needed.
|
||||||
|
*
|
||||||
|
* Stored in ~/.openclaw/openclaw.json under the plugin's `anonymousTelemetryId`
|
||||||
|
* field so repeat sessions on the same machine share one PostHog identity
|
||||||
|
* instead of collapsing into a single shared fallback string. The result is
|
||||||
|
* cached in module memory after the first read so we don't re-touch disk on
|
||||||
|
* every queued event.
|
||||||
|
*/
|
||||||
|
function getOrCreateAnonymousId(): string {
|
||||||
|
if (_cachedAnonymousId) return _cachedAnonymousId;
|
||||||
|
try {
|
||||||
|
const auth = readPluginAuth();
|
||||||
|
if (auth.anonymousTelemetryId) {
|
||||||
|
_cachedAnonymousId = auth.anonymousTelemetryId;
|
||||||
|
return _cachedAnonymousId;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
const newId = `openclaw-anon-${randomUUID().replace(/-/g, "")}`;
|
||||||
|
try {
|
||||||
|
writePluginAuth({ anonymousTelemetryId: newId });
|
||||||
|
} catch {
|
||||||
|
/* ignore — return generated id anyway */
|
||||||
|
}
|
||||||
|
_cachedAnonymousId = newId;
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we just resolved to a real identity but a stored anonymous id exists,
|
||||||
|
* build a one-shot PostHog $identify event so the pre-signup history gets
|
||||||
|
* stitched onto the authenticated profile. Returns null when no aliasing is
|
||||||
|
* needed (already done, or no anon id on disk, or still anonymous).
|
||||||
|
*
|
||||||
|
* Caller is responsible for pushing the returned event onto eventQueue ahead
|
||||||
|
* of the regular event.
|
||||||
|
*/
|
||||||
|
function maybeBuildIdentifyEvent(
|
||||||
|
distinctId: string,
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
if (_aliasCheckDone) return null;
|
||||||
|
if (!distinctId || distinctId.startsWith("openclaw-anon-")) return null;
|
||||||
|
try {
|
||||||
|
const auth = readPluginAuth();
|
||||||
|
const storedAnon = auth.anonymousTelemetryId;
|
||||||
|
if (!storedAnon) {
|
||||||
|
_aliasCheckDone = true;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const identifyEvent = {
|
||||||
|
event: "$identify",
|
||||||
|
distinct_id: distinctId,
|
||||||
|
properties: {
|
||||||
|
$anon_distinct_id: storedAnon,
|
||||||
|
$lib: "posthog-node",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
writePluginAuth({ anonymousTelemetryId: "" });
|
||||||
|
} catch {
|
||||||
|
/* ignore — alias may double-fire next session, harmless */
|
||||||
|
}
|
||||||
|
_aliasCheckDone = true;
|
||||||
|
_cachedAnonymousId = undefined;
|
||||||
|
return identifyEvent;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _emailResolutionAttempted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we have an apiKey but no cached userEmail, do a one-shot /v1/ping/
|
||||||
|
* call to resolve the email and cache it. This runs async as a side-effect;
|
||||||
|
* the current event ships with md5(apiKey) but subsequent events (including
|
||||||
|
* those flushed by the beforeExit handler in the same process) will use
|
||||||
|
* the resolved email.
|
||||||
|
*/
|
||||||
|
function maybeResolveEmail(apiKey: string): void {
|
||||||
|
if (_emailResolutionAttempted) return;
|
||||||
|
_emailResolutionAttempted = true;
|
||||||
|
|
||||||
|
const baseUrl = getBaseUrl().replace(/\/+$/, "");
|
||||||
|
fetch(`${baseUrl}/v1/ping/`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Token ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(5_000),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data: any) => {
|
||||||
|
const email = data?.user_email;
|
||||||
|
if (email) {
|
||||||
|
try {
|
||||||
|
writePluginAuth({ userEmail: email });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
// Upgrade any already-queued events from md5(apiKey) to email
|
||||||
|
const oldId = createHash("md5").update(apiKey).digest("hex");
|
||||||
|
for (const ev of eventQueue) {
|
||||||
|
if (ev.distinct_id === oldId) {
|
||||||
|
ev.distinct_id = email;
|
||||||
|
}
|
||||||
|
// Also upgrade $identify's distinct_id if present
|
||||||
|
if (
|
||||||
|
ev.event === "$identify" &&
|
||||||
|
ev.distinct_id === oldId
|
||||||
|
) {
|
||||||
|
ev.distinct_id = email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* silently swallow — md5(apiKey) is used as fallback */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let _telemetryEnabled: boolean | undefined;
|
let _telemetryEnabled: boolean | undefined;
|
||||||
function isTelemetryEnabled(): boolean {
|
function isTelemetryEnabled(): boolean {
|
||||||
if (_telemetryEnabled !== undefined) return _telemetryEnabled;
|
if (_telemetryEnabled !== undefined) return _telemetryEnabled;
|
||||||
@@ -42,7 +169,8 @@ function isTelemetryEnabled(): boolean {
|
|||||||
/**
|
/**
|
||||||
* Return a stable anonymous identifier for the current user.
|
* Return a stable anonymous identifier for the current user.
|
||||||
*
|
*
|
||||||
* Priority: cached userEmail (from /v1/ping/) > MD5(apiKey) > fallback.
|
* Priority: cached userEmail (from /v1/ping/) > MD5(apiKey) >
|
||||||
|
* persistent per-machine anonymous ID.
|
||||||
*/
|
*/
|
||||||
function getDistinctId(apiKey?: string): string {
|
function getDistinctId(apiKey?: string): string {
|
||||||
try {
|
try {
|
||||||
@@ -54,7 +182,7 @@ function getDistinctId(apiKey?: string): string {
|
|||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
return createHash("md5").update(apiKey).digest("hex");
|
return createHash("md5").update(apiKey).digest("hex");
|
||||||
}
|
}
|
||||||
return "anonymous-openclaw";
|
return getOrCreateAnonymousId();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureFlushTimer(): void {
|
function ensureFlushTimer(): void {
|
||||||
@@ -65,6 +193,42 @@ function ensureFlushTimer(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _exitHandlerInstalled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a one-time `beforeExit` handler that drains queued events on
|
||||||
|
* process exit. Without this, short-lived CLI invocations (e.g. one
|
||||||
|
* `openclaw mem0 status` call) exit before the unref'd flushTimer fires
|
||||||
|
* and before FLUSH_THRESHOLD is hit, dropping every queued event silently.
|
||||||
|
*
|
||||||
|
* Returning a Promise from a `beforeExit` handler keeps the event loop
|
||||||
|
* alive until that Promise resolves, so the awaited fetch actually has
|
||||||
|
* time to land at PostHog.
|
||||||
|
*/
|
||||||
|
function ensureExitHandler(): void {
|
||||||
|
if (_exitHandlerInstalled) return;
|
||||||
|
_exitHandlerInstalled = true;
|
||||||
|
process.on("beforeExit", async () => {
|
||||||
|
if (eventQueue.length === 0) return;
|
||||||
|
const batch = eventQueue;
|
||||||
|
eventQueue = [];
|
||||||
|
const body = JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
|
||||||
|
try {
|
||||||
|
await fetch(POSTHOG_HOST, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": String(Buffer.byteLength(body)),
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
signal: AbortSignal.timeout(3_000),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* silently swallow */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function flushEvents(): void {
|
function flushEvents(): void {
|
||||||
if (eventQueue.length === 0) return;
|
if (eventQueue.length === 0) return;
|
||||||
const batch = eventQueue;
|
const batch = eventQueue;
|
||||||
@@ -97,6 +261,22 @@ export function captureEvent(
|
|||||||
try {
|
try {
|
||||||
const distinctId = getDistinctId(ctx?.apiKey);
|
const distinctId = getDistinctId(ctx?.apiKey);
|
||||||
|
|
||||||
|
// If we resolved to md5(apiKey) instead of email, kick off a background
|
||||||
|
// /v1/ping/ to resolve and cache the email. The current event ships with
|
||||||
|
// the hash, but the async resolution upgrades any still-queued events
|
||||||
|
// (including this one) before the beforeExit flush fires.
|
||||||
|
if (ctx?.apiKey && distinctId && !distinctId.includes("@") && !distinctId.startsWith("openclaw-anon-")) {
|
||||||
|
maybeResolveEmail(ctx.apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First authenticated event after a previous anonymous session: queue a
|
||||||
|
// $identify ahead of the regular event so PostHog merges the anonymous
|
||||||
|
// history onto the authenticated profile in the same batch flush.
|
||||||
|
const identifyEvent = maybeBuildIdentifyEvent(distinctId);
|
||||||
|
if (identifyEvent) {
|
||||||
|
eventQueue.push(identifyEvent);
|
||||||
|
}
|
||||||
|
|
||||||
eventQueue.push({
|
eventQueue.push({
|
||||||
event: eventName,
|
event: eventName,
|
||||||
distinct_id: distinctId,
|
distinct_id: distinctId,
|
||||||
@@ -115,6 +295,7 @@ export function captureEvent(
|
|||||||
});
|
});
|
||||||
|
|
||||||
ensureFlushTimer();
|
ensureFlushTimer();
|
||||||
|
ensureExitHandler();
|
||||||
|
|
||||||
if (eventQueue.length >= FLUSH_THRESHOLD) {
|
if (eventQueue.length >= FLUSH_THRESHOLD) {
|
||||||
flushEvents();
|
flushEvents();
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ describe("providerToBackend — search", () => {
|
|||||||
keyword_search: true,
|
keyword_search: true,
|
||||||
reranking: true,
|
reranking: true,
|
||||||
filters: { category: "preference" },
|
filters: { category: "preference" },
|
||||||
|
source: "OPENCLAW",
|
||||||
});
|
});
|
||||||
expect(results).toHaveLength(1);
|
expect(results).toHaveLength(1);
|
||||||
expect((results[0] as any).id).toBe("m1");
|
expect((results[0] as any).id).toBe("m1");
|
||||||
@@ -79,6 +80,7 @@ describe("providerToBackend — search", () => {
|
|||||||
keyword_search: undefined,
|
keyword_search: undefined,
|
||||||
reranking: undefined,
|
reranking: undefined,
|
||||||
filters: undefined,
|
filters: undefined,
|
||||||
|
source: "OPENCLAW",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -173,6 +175,7 @@ describe("providerToBackend — listMemories", () => {
|
|||||||
expect(provider.getAll).toHaveBeenCalledWith({
|
expect(provider.getAll).toHaveBeenCalledWith({
|
||||||
user_id: DEFAULT_USER,
|
user_id: DEFAULT_USER,
|
||||||
page_size: 50,
|
page_size: 50,
|
||||||
|
source: "OPENCLAW",
|
||||||
});
|
});
|
||||||
expect(results).toHaveLength(1);
|
expect(results).toHaveLength(1);
|
||||||
});
|
});
|
||||||
@@ -186,6 +189,7 @@ describe("providerToBackend — listMemories", () => {
|
|||||||
expect(provider.getAll).toHaveBeenCalledWith({
|
expect(provider.getAll).toHaveBeenCalledWith({
|
||||||
user_id: DEFAULT_USER,
|
user_id: DEFAULT_USER,
|
||||||
page_size: undefined,
|
page_size: undefined,
|
||||||
|
source: "OPENCLAW",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ describe("telemetry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("exports PLUGIN_VERSION", () => {
|
it("exports PLUGIN_VERSION", () => {
|
||||||
expect(PLUGIN_VERSION).toBe("1.0.4");
|
expect(PLUGIN_VERSION).toBe("1.0.6");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("captureEvent does not throw", () => {
|
it("captureEvent does not throw", () => {
|
||||||
@@ -51,7 +51,7 @@ describe("telemetry", () => {
|
|||||||
expect(() => captureEvent("test_event")).not.toThrow();
|
expect(() => captureEvent("test_event")).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to anonymous-openclaw when no apiKey", () => {
|
it("falls back to a generated anonymous id when no apiKey", () => {
|
||||||
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({});
|
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({});
|
||||||
expect(() => captureEvent("test_event", {}, {})).not.toThrow();
|
expect(() => captureEvent("test_event", {}, {})).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user