@betterdb/semantic-cache 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,14 +11,35 @@ export interface ValkeyLike {
11
11
  export interface Analytics {
12
12
  init(client: ValkeyLike, name: string, configProps?: Record<string, unknown>): Promise<void>;
13
13
  capture(event: string, properties?: Record<string, unknown>): void;
14
+ flush(): Promise<void>;
14
15
  shutdown(): Promise<void>;
15
16
  }
16
17
  export interface AnalyticsOptions {
17
- apiKey?: string;
18
- host?: string;
19
18
  disabled?: boolean;
20
19
  /** Interval in ms for periodic stats snapshots. Default: 300_000 (5 min). 0 to disable. */
21
20
  statsIntervalMs?: number;
22
21
  }
23
22
  export declare const NOOP_ANALYTICS: Analytics;
23
+ type PostHogClient = {
24
+ capture: (opts: {
25
+ distinctId?: string;
26
+ event: string;
27
+ properties?: Record<string, unknown>;
28
+ }) => void;
29
+ flush: () => Promise<void>;
30
+ shutdown: () => Promise<void>;
31
+ };
32
+ export declare class PostHogAnalytics implements Analytics {
33
+ private posthog;
34
+ private distinctId;
35
+ private deploymentId;
36
+ private readonly flushOnExit;
37
+ constructor(posthog: PostHogClient);
38
+ init(client: ValkeyLike, name: string, configProps?: Record<string, unknown>): Promise<void>;
39
+ private resolveDeploymentId;
40
+ capture(event: string, properties?: Record<string, unknown>): void;
41
+ flush(): Promise<void>;
42
+ shutdown(): Promise<void>;
43
+ }
24
44
  export declare function createAnalytics(opts?: AnalyticsOptions): Promise<Analytics>;
45
+ export {};
package/dist/analytics.js CHANGED
@@ -39,8 +39,11 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.NOOP_ANALYTICS = void 0;
42
+ exports.PostHogAnalytics = exports.NOOP_ANALYTICS = void 0;
43
43
  exports.createAnalytics = createAnalytics;
44
+ const node_fs_1 = require("node:fs");
45
+ const node_os_1 = require("node:os");
46
+ const node_path_1 = require("node:path");
44
47
  const EVENT_PREFIX = 'semantic_cache:';
45
48
  // Build-time placeholders — replaced by scripts/inject-telemetry-defaults.mjs
46
49
  // When the placeholder is NOT replaced, the startsWith('__') guard treats it as unset.
@@ -49,41 +52,126 @@ const BAKED_POSTHOG_HOST = '__BETTERDB_POSTHOG_HOST__';
49
52
  exports.NOOP_ANALYTICS = {
50
53
  async init() { },
51
54
  capture() { },
55
+ async flush() { },
52
56
  async shutdown() { },
53
57
  };
54
58
  function isTelemetryOptedOut() {
55
59
  const val = process.env.BETTERDB_TELEMETRY;
56
60
  return val !== undefined && ['false', '0', 'no', 'off'].includes(val.toLowerCase());
57
61
  }
62
+ const INSTALL_ID_ENV = 'BETTERDB_INSTANCE_ID';
63
+ // Holds a minted id for the rest of the process when persistence fails, so
64
+ // repeated calls (or parallel init) return one stable ephemeral identity.
65
+ let ephemeralInstallId;
66
+ function installIdPath() {
67
+ const base = process.env.XDG_STATE_HOME;
68
+ const root = base ? base : (0, node_path_1.join)((0, node_os_1.homedir)(), '.betterdb');
69
+ return (0, node_path_1.join)(root, 'instance_id');
70
+ }
71
+ /**
72
+ * Stable per-install identity for product analytics. Persisted on the local
73
+ * machine (not in Valkey), so a fleet of processes sharing one Valkey is
74
+ * counted as many installs rather than collapsing to one. Pin it via
75
+ * BETTERDB_INSTANCE_ID for ephemeral containers that would otherwise mint a
76
+ * fresh id every run. Falls back to an ephemeral per-process id when no
77
+ * writable location is available.
78
+ */
79
+ function getInstallId() {
80
+ const override = process.env[INSTALL_ID_ENV];
81
+ if (override)
82
+ return override;
83
+ const path = installIdPath();
84
+ try {
85
+ const existing = (0, node_fs_1.readFileSync)(path, 'utf8').trim();
86
+ if (existing)
87
+ return existing;
88
+ }
89
+ catch {
90
+ // no existing id
91
+ }
92
+ const newId = ephemeralInstallId ?? crypto.randomUUID();
93
+ try {
94
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
95
+ (0, node_fs_1.writeFileSync)(path, newId);
96
+ }
97
+ catch {
98
+ // Persistence failed — hold the id for the rest of this process so
99
+ // repeated calls return a stable ephemeral identity.
100
+ ephemeralInstallId = newId;
101
+ }
102
+ return newId;
103
+ }
58
104
  class PostHogAnalytics {
59
105
  posthog;
60
106
  distinctId = '';
107
+ deploymentId = '';
108
+ // Library consumers are frequently short-lived scripts that never call
109
+ // shutdown(), so PostHog's buffered events (flushAt=20, flushInterval=10s)
110
+ // would be dropped when the process exits before the queue drains. Flush
111
+ // when the event loop empties so lifecycle events are actually delivered.
112
+ // Only enabled instances reach here — the opt-out path returns
113
+ // NOOP_ANALYTICS and registers nothing, keeping disabled consumers silent.
114
+ flushOnExit = () => {
115
+ void this.flush();
116
+ };
61
117
  constructor(posthog) {
62
118
  this.posthog = posthog;
119
+ process.once('beforeExit', this.flushOnExit);
63
120
  }
64
121
  async init(client, name, configProps) {
122
+ this.distinctId = getInstallId();
123
+ this.deploymentId = await this.resolveDeploymentId(client, name);
124
+ const merged = { ...(configProps ?? {}) };
125
+ if (this.deploymentId)
126
+ merged.deployment_id = this.deploymentId;
127
+ this.capture('cache_init', merged);
128
+ // Flush the start event immediately so it lands even for processes that exit
129
+ // before the flush interval or the beforeExit hook fires.
130
+ await this.flush();
131
+ }
132
+ async resolveDeploymentId(client, name) {
133
+ // The Valkey-scoped id groups all clients pointed at the same store, so a
134
+ // shared-Valkey fleet can still be rolled up into one deployment.
65
135
  const idKey = `${name}:__instance_id`;
66
- let id = await client.get(idKey);
67
- if (!id) {
68
- id = crypto.randomUUID();
136
+ try {
137
+ const existing = await client.get(idKey);
138
+ if (existing)
139
+ return existing;
140
+ const id = crypto.randomUUID();
69
141
  await client.set(idKey, id);
142
+ return id;
143
+ }
144
+ catch {
145
+ return '';
70
146
  }
71
- this.distinctId = id;
72
- this.capture('cache_init', configProps);
73
147
  }
74
148
  capture(event, properties) {
75
149
  try {
150
+ const props = { ...(properties ?? {}) };
151
+ if (this.deploymentId && props.deployment_id === undefined) {
152
+ props.deployment_id = this.deploymentId;
153
+ }
76
154
  this.posthog.capture({
77
155
  distinctId: this.distinctId,
78
156
  event: `${EVENT_PREFIX}${event}`,
79
- properties,
157
+ properties: props,
80
158
  });
81
159
  }
82
160
  catch {
83
161
  // never throw from analytics
84
162
  }
85
163
  }
164
+ async flush() {
165
+ try {
166
+ await this.posthog.flush();
167
+ }
168
+ catch {
169
+ // swallow
170
+ }
171
+ }
86
172
  async shutdown() {
173
+ // Explicit shutdown supersedes the beforeExit backstop.
174
+ process.removeListener('beforeExit', this.flushOnExit);
87
175
  try {
88
176
  await this.posthog.shutdown();
89
177
  }
@@ -92,18 +180,16 @@ class PostHogAnalytics {
92
180
  }
93
181
  }
94
182
  }
183
+ exports.PostHogAnalytics = PostHogAnalytics;
95
184
  async function createAnalytics(opts) {
96
185
  if (opts?.disabled || isTelemetryOptedOut()) {
97
186
  return exports.NOOP_ANALYTICS;
98
187
  }
99
- // Key resolution: opts.apiKey BETTERDB_POSTHOG_API_KEY env var → baked wheel value
100
- const bakedKey = BAKED_POSTHOG_API_KEY.startsWith('__') ? undefined : BAKED_POSTHOG_API_KEY;
101
- const apiKey = opts?.apiKey ?? process.env.BETTERDB_POSTHOG_API_KEY ?? bakedKey;
188
+ const apiKey = BAKED_POSTHOG_API_KEY.startsWith('__') ? undefined : BAKED_POSTHOG_API_KEY;
102
189
  if (!apiKey) {
103
190
  return exports.NOOP_ANALYTICS;
104
191
  }
105
- const bakedHost = BAKED_POSTHOG_HOST.startsWith('__') ? undefined : BAKED_POSTHOG_HOST;
106
- const host = opts?.host ?? process.env.BETTERDB_POSTHOG_HOST ?? bakedHost;
192
+ const host = BAKED_POSTHOG_HOST.startsWith('__') ? undefined : BAKED_POSTHOG_HOST;
107
193
  try {
108
194
  // @ts-ignore — posthog-node is an optional peer dep
109
195
  const { PostHog } = await Promise.resolve().then(() => __importStar(require('posthog-node')));
package/dist/types.d.ts CHANGED
@@ -90,10 +90,6 @@ export interface SemanticCacheOptions {
90
90
  registry?: Registry;
91
91
  };
92
92
  analytics?: {
93
- /** PostHog API key. Overrides the build-time baked key if set. */
94
- apiKey?: string;
95
- /** PostHog host. Overrides the build-time baked host if set. */
96
- host?: string;
97
93
  /** Disable analytics. Also controlled by BETTERDB_TELEMETRY env var. */
98
94
  disabled?: boolean;
99
95
  /** Interval in ms for periodic stats snapshots. Default: 300_000 (5 min). 0 to disable. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterdb/semantic-cache",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Valkey-native semantic cache for LLM applications with built-in OpenTelemetry and Prometheus instrumentation",
5
5
  "keywords": [
6
6
  "valkey",