@dr-sentry/sdk 1.0.4 → 1.1.1

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.
Files changed (54) hide show
  1. package/README.md +101 -4
  2. package/dist/cjs/cache.d.ts +16 -0
  3. package/dist/cjs/cache.d.ts.map +1 -1
  4. package/dist/cjs/cache.js +21 -1
  5. package/dist/cjs/cache.js.map +1 -1
  6. package/dist/cjs/client.d.ts +46 -1
  7. package/dist/cjs/client.d.ts.map +1 -1
  8. package/dist/cjs/client.js +236 -9
  9. package/dist/cjs/client.js.map +1 -1
  10. package/dist/cjs/defaults.d.ts +58 -0
  11. package/dist/cjs/defaults.d.ts.map +1 -0
  12. package/dist/cjs/defaults.js +175 -0
  13. package/dist/cjs/defaults.js.map +1 -0
  14. package/dist/cjs/index.d.ts +2 -1
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js +4 -1
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/status-reporter.d.ts +42 -0
  19. package/dist/cjs/status-reporter.d.ts.map +1 -0
  20. package/dist/cjs/status-reporter.js +153 -0
  21. package/dist/cjs/status-reporter.js.map +1 -0
  22. package/dist/cjs/streaming.d.ts +8 -2
  23. package/dist/cjs/streaming.d.ts.map +1 -1
  24. package/dist/cjs/streaming.js +17 -4
  25. package/dist/cjs/streaming.js.map +1 -1
  26. package/dist/cjs/types.d.ts +65 -0
  27. package/dist/cjs/types.d.ts.map +1 -1
  28. package/dist/esm/cache.d.ts +16 -0
  29. package/dist/esm/cache.d.ts.map +1 -1
  30. package/dist/esm/cache.js +21 -1
  31. package/dist/esm/cache.js.map +1 -1
  32. package/dist/esm/client.d.ts +46 -1
  33. package/dist/esm/client.d.ts.map +1 -1
  34. package/dist/esm/client.js +236 -9
  35. package/dist/esm/client.js.map +1 -1
  36. package/dist/esm/defaults.d.ts +58 -0
  37. package/dist/esm/defaults.d.ts.map +1 -0
  38. package/dist/esm/defaults.js +136 -0
  39. package/dist/esm/defaults.js.map +1 -0
  40. package/dist/esm/index.d.ts +2 -1
  41. package/dist/esm/index.d.ts.map +1 -1
  42. package/dist/esm/index.js +1 -0
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/esm/status-reporter.d.ts +42 -0
  45. package/dist/esm/status-reporter.d.ts.map +1 -0
  46. package/dist/esm/status-reporter.js +148 -0
  47. package/dist/esm/status-reporter.js.map +1 -0
  48. package/dist/esm/streaming.d.ts +8 -2
  49. package/dist/esm/streaming.d.ts.map +1 -1
  50. package/dist/esm/streaming.js +17 -4
  51. package/dist/esm/streaming.js.map +1 -1
  52. package/dist/esm/types.d.ts +65 -0
  53. package/dist/esm/types.d.ts.map +1 -1
  54. package/package.json +1 -1
package/README.md CHANGED
@@ -40,6 +40,42 @@ const config = await client.jsonValue('rate-limits', { rpm: 100 });
40
40
  client.close();
41
41
  ```
42
42
 
43
+ ## Status reporting (optional)
44
+
45
+ Enable `reportStatus` to have the SDK push version + health to DeploySentry automatically. No separate startup code needed — the SDK posts `POST /applications/:id/status` on init and on an interval.
46
+
47
+ ```typescript
48
+ const client = new DeploySentryClient({
49
+ apiKey: process.env.DS_API_KEY!,
50
+ environment: 'production',
51
+ project: 'my-app',
52
+ application: 'my-web-app',
53
+
54
+ // Agentless status reporting
55
+ applicationId: process.env.DS_APPLICATION_ID!, // UUID of the app
56
+ reportStatus: true,
57
+ reportStatusIntervalMs: 30_000, // default; 0 = startup-only
58
+ reportStatusVersion: process.env.APP_VERSION, // optional override
59
+ reportStatusCommitSha: process.env.GIT_SHA,
60
+ reportStatusDeploySlot: process.env.DS_DEPLOY_SLOT, // "stable" | "canary"
61
+ reportStatusTags: { region: process.env.REGION ?? 'unknown' },
62
+ reportStatusHealthProvider: async () => ({
63
+ state: (await isDbUp()) ? 'healthy' : 'degraded',
64
+ score: 0.99,
65
+ }),
66
+ });
67
+
68
+ await client.initialize();
69
+ ```
70
+
71
+ The API key must carry the `status:write` scope and be scoped to a single application + environment. When `reportStatus` is `true` but `applicationId` is omitted, the reporter logs a warning and is disabled — flag evaluation continues to work.
72
+
73
+ Without a `reportStatusHealthProvider`, the SDK sends `health: "healthy"` on every tick ("process alive" floor). Supply a provider if your app can detect degradation (stale DB, downstream outages, etc.).
74
+
75
+ Version auto-detection (when `reportStatusVersion` is omitted) checks these env vars in order: `APP_VERSION`, `GIT_SHA`, `GIT_COMMIT`, `SOURCE_COMMIT`, `RAILWAY_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`, `VERCEL_GIT_COMMIT_SHA`, `HEROKU_SLUG_COMMIT`, `npm_package_version` — falling back to the literal string `"unknown"`.
76
+
77
+ The first `/status` report with a version DeploySentry has not seen auto-creates a `mode=record` deployment with `source="app-push"`, so deploy history populates even without a Railway/Render/etc. webhook configured.
78
+
43
79
  ## Evaluation Context
44
80
 
45
81
  Pass targeting context with every evaluation:
@@ -113,6 +149,36 @@ List all cached flags:
113
149
  const flags = client.allFlags();
114
150
  ```
115
151
 
152
+ ## Inspecting Flag Sources (admin / support view)
153
+
154
+ `inspect()` returns a provenance-tagged snapshot of every flag the SDK currently knows about — drawn from all sources at once (live server cache, offline file config, and offline defaults). Each entry reports the resolved value, its type, **where the value came from**, an evaluation reason, and when it was last fetched. Wire it into an admin or support panel to see exactly what an application is serving and why.
155
+
156
+ ```typescript
157
+ const view = client.inspect();
158
+ // [
159
+ // {
160
+ // key: 'new-checkout',
161
+ // value: true,
162
+ // type: 'boolean',
163
+ // enabled: true,
164
+ // source: 'server', // 'server' | 'cache' | 'file' | 'default'
165
+ // reason: 'LIVE', // LIVE | CACHE | OFFLINE_FILE | OFFLINE_DEFAULT | DEFAULT
166
+ // fetchedAt: '2026-06-23T12:00:00.000Z',
167
+ // stale: false, // true when a cached value is past its TTL
168
+ // },
169
+ // ...
170
+ // ]
171
+ ```
172
+
173
+ | `source` | Meaning |
174
+ | --- | --- |
175
+ | `server` | Fetched live from the API and still within its cache TTL. |
176
+ | `cache` | A previously-fetched server value past its TTL, still served because the server is currently unreachable (`stale: true`). |
177
+ | `file` | An offline file — a `mode: 'file'` config, or the offline defaults loaded via `loadDefaults*()` (the `reason` distinguishes `OFFLINE_FILE` vs `OFFLINE_DEFAULT`). |
178
+ | `default` | No value from any source; callers receive their coded `defaultValue`. |
179
+
180
+ `inspect()` is a read-only snapshot — it does not contact the server or evaluate targeting rules for a specific user.
181
+
116
182
  ## Register / Dispatch Pattern
117
183
 
118
184
  The register/dispatch pattern lets you centralize all flag-gated behavior in one place instead of scattering `if/else` checks throughout your codebase. Register named operations with their handlers and optional flag keys, then dispatch by operation name at the call site.
@@ -191,6 +257,37 @@ const value = await client.boolValue('any-flag', true);
191
257
  // Returns the default value: true
192
258
  ```
193
259
 
260
+ ## Offline Defaults
261
+
262
+ For deployments where the SDK may briefly be unable to reach DeploySentry (CI runs without network, cold-start before SSE connects, air-gapped builds), commit a flag snapshot to your repo and load it as fallback. The SDK still prefers the live API; defaults only fire when both the API and the in-process cache are empty.
263
+
264
+ Generate the snapshot with the CLI:
265
+
266
+ ```bash
267
+ deploysentry flags export --application web --env production -f flags.json
268
+ ```
269
+
270
+ Then load it after constructing the client:
271
+
272
+ ```typescript
273
+ const client = new DeploySentryClient({
274
+ apiKey: process.env.DEPLOYSENTRY_API_KEY!,
275
+ environment: 'production',
276
+ project: 'my-app',
277
+ application: 'web',
278
+ });
279
+ await client.loadDefaultsFromFile('flags.json');
280
+ // or sync from an already-parsed object:
281
+ // client.loadDefaults(JSON.parse(fs.readFileSync('flags.json', 'utf-8')));
282
+ ```
283
+
284
+ Evaluation order:
285
+
286
+ 1. **Live API** when reachable.
287
+ 2. **In-process cache** (entries from prior live calls).
288
+ 3. **Offline defaults** loaded above — picks the per-environment value matching your `environment` option (by UUID first, then by name, case-insensitive). Falls back to the flag's global `default_value` when no env matches.
289
+ 4. **Caller's `defaultValue`** when the flag isn't in the export.
290
+
194
291
  ## Configuration
195
292
 
196
293
  | Option | Type | Required | Default | Description |
@@ -207,15 +304,15 @@ const value = await client.boolValue('any-flag', true);
207
304
 
208
305
  ## Offline / File Mode
209
306
 
210
- The SDK can load flag configurations from a local YAML file instead of (or as fallback to) the server.
307
+ The SDK can load flag configurations from a local YAML/JSON file. **The server is always the source of truth** the exported file is a *backup/fallback only*. In production use `server-with-fallback`: the SDK reaches the server first on every evaluation and consults the local file only when the server (and the in-process cache) are unreachable. Pure `file` mode (no server contact) is intended for local development, CI, and testing — not for normal production operation.
211
308
 
212
309
  ### Modes
213
310
 
214
311
  | Mode | Behavior |
215
312
  | --- | --- |
216
- | `server` (default) | API calls + SSE streaming |
217
- | `file` | Load from YAML file, evaluate locally. No server contact. |
218
- | `server-with-fallback` | Try server first. If unavailable, fall back to YAML file. |
313
+ | `server` (default) | API calls + SSE streaming. Server only. |
314
+ | `file` | Load from local file, evaluate locally. **No server contact — dev/CI/testing only.** |
315
+ | `server-with-fallback` | **Recommended for resilience.** Always try the server first; fall back to the local file only when the server is unreachable, then return to live as soon as it recovers. |
219
316
 
220
317
  ### Usage
221
318
 
@@ -1,4 +1,14 @@
1
1
  import { Flag } from './types';
2
+ /** A cache entry exposed for inspection, including staleness. */
3
+ export interface CacheInspectionEntry {
4
+ flag: Flag;
5
+ /** Epoch-ms when the entry was last written. */
6
+ storedAt: number;
7
+ /** Epoch-ms when the entry's TTL elapses. */
8
+ expiresAt: number;
9
+ /** True when the TTL has elapsed but the entry is still held. */
10
+ stale: boolean;
11
+ }
2
12
  /**
3
13
  * In-memory flag cache with per-entry TTL support.
4
14
  *
@@ -21,6 +31,12 @@ export declare class FlagCache {
21
31
  get(key: string): Flag | undefined;
22
32
  /** Return all non-expired flags currently in the cache. */
23
33
  getAll(): Flag[];
34
+ /**
35
+ * Return every entry held in the cache — including stale (TTL-elapsed)
36
+ * entries — without evicting anything. For inspection / admin views only;
37
+ * read paths should use {@link get} / {@link getAll} which evict.
38
+ */
39
+ entries(): CacheInspectionEntry[];
24
40
  /** Remove a single key from the cache. */
25
41
  delete(key: string): void;
26
42
  /** Remove all expired entries. Returns the number of entries purged. */
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAO/B;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B;;;OAGG;gBACS,KAAK,GAAE,MAAe;IAIlC,kDAAkD;IAClD,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAOrB,sDAAsD;IACtD,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAM5B,yEAAyE;IACzE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAYlC,2DAA2D;IAC3D,MAAM,IAAI,IAAI,EAAE;IAehB,0CAA0C;IAC1C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,wEAAwE;IACxE,YAAY,IAAI,MAAM;IActB,uCAAuC;IACvC,KAAK,IAAI,IAAI;IAIb,8DAA8D;IAC9D,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAQ/B,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,IAAI,CAAC;IACX,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B;;;OAGG;gBACS,KAAK,GAAE,MAAe;IAIlC,kDAAkD;IAClD,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IASrB,sDAAsD;IACtD,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAM5B,yEAAyE;IACzE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAYlC,2DAA2D;IAC3D,MAAM,IAAI,IAAI,EAAE;IAehB;;;;OAIG;IACH,OAAO,IAAI,oBAAoB,EAAE;IAcjC,0CAA0C;IAC1C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,wEAAwE;IACxE,YAAY,IAAI,MAAM;IActB,uCAAuC;IACvC,KAAK,IAAI,IAAI;IAIb,8DAA8D;IAC9D,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
package/dist/cjs/cache.js CHANGED
@@ -19,9 +19,11 @@ class FlagCache {
19
19
  }
20
20
  /** Store or update a single flag in the cache. */
21
21
  set(flag) {
22
+ const now = Date.now();
22
23
  this.store.set(flag.key, {
23
24
  flag,
24
- expiresAt: Date.now() + this.ttlMs,
25
+ storedAt: now,
26
+ expiresAt: now + this.ttlMs,
25
27
  });
26
28
  }
27
29
  /** Bulk-insert flags, replacing any stale entries. */
@@ -55,6 +57,24 @@ class FlagCache {
55
57
  }
56
58
  return result;
57
59
  }
60
+ /**
61
+ * Return every entry held in the cache — including stale (TTL-elapsed)
62
+ * entries — without evicting anything. For inspection / admin views only;
63
+ * read paths should use {@link get} / {@link getAll} which evict.
64
+ */
65
+ entries() {
66
+ const now = Date.now();
67
+ const result = [];
68
+ for (const entry of this.store.values()) {
69
+ result.push({
70
+ flag: entry.flag,
71
+ storedAt: entry.storedAt,
72
+ expiresAt: entry.expiresAt,
73
+ stale: now > entry.expiresAt,
74
+ });
75
+ }
76
+ return result;
77
+ }
58
78
  /** Remove a single key from the cache. */
59
79
  delete(key) {
60
80
  this.store.delete(key);
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":";;;AAOA;;;;;GAKG;AACH,MAAa,SAAS;IACH,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IACtC,KAAK,CAAS;IAE/B;;;OAGG;IACH,YAAY,QAAgB,MAAM;QAChC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,kDAAkD;IAClD,GAAG,CAAC,IAAU;QACZ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE;YACvB,IAAI;YACJ,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK;SACnC,CAAC,CAAC;IACL,CAAC;IAED,sDAAsD;IACtD,OAAO,CAAC,KAAa;QACnB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,GAAG,CAAC,GAAW;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAE7B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC;IACpB,CAAC;IAED,2DAA2D;IAC3D,MAAM;QACJ,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAW,EAAE,CAAC;QAE1B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACtC,IAAI,GAAG,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;gBAC1B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,0CAA0C;IAC1C,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,wEAAwE;IACxE,YAAY;QACV,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACtC,IAAI,GAAG,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;gBAC1B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACvB,MAAM,EAAE,CAAC;YACX,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,uCAAuC;IACvC,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,8DAA8D;IAC9D,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;CACF;AArFD,8BAqFC"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { ClientOptions, EvaluationContext, EvaluationResult, Flag, FlagCategory } from './types';
1
+ import { ClientOptions, EvaluationContext, EvaluationResult, Flag, FlagCategory, FlagInspection } from './types';
2
2
  /**
3
3
  * DeploySentry feature-flag client.
4
4
  *
@@ -26,11 +26,18 @@ export declare class DeploySentryClient {
26
26
  private readonly sessionId;
27
27
  private readonly mode;
28
28
  private readonly flagFilePath?;
29
+ private readonly onFlagChange?;
29
30
  private readonly cache;
30
31
  private streamClient;
31
32
  private _initialized;
32
33
  private flagConfig;
34
+ private offlineDefaults;
35
+ private offlineDefaultsEnvUUID;
36
+ private flagConfigLoadedAt?;
37
+ private offlineDefaultsLoadedAt?;
33
38
  private registry;
39
+ private statusReporter;
40
+ private readonly statusReporterOptions;
34
41
  constructor(options: ClientOptions);
35
42
  /**
36
43
  * Fetch the initial flag set and open an SSE connection for real-time
@@ -39,6 +46,7 @@ export declare class DeploySentryClient {
39
46
  initialize(): Promise<void>;
40
47
  /** Tear down the SSE connection and release resources. */
41
48
  close(): void;
49
+ private startStatusReporter;
42
50
  /**
43
51
  * Clear the local cache and re-fetch all flags from the server.
44
52
  * Useful when session state may have changed and the client needs
@@ -68,10 +76,47 @@ export declare class DeploySentryClient {
68
76
  flagOwners(key: string): string[];
69
77
  /** Return every flag currently held in the local cache. */
70
78
  allFlags(): Flag[];
79
+ /**
80
+ * Return a provenance-tagged view of every flag the SDK currently knows
81
+ * about, drawn from all sources (live server cache, offline file config,
82
+ * and offline defaults). Each entry reports the resolved value, its type,
83
+ * the {@link FlagSource | source} it came from, an evaluation reason code,
84
+ * and when it was last fetched / loaded.
85
+ *
86
+ * Intended for wiring into an admin or support panel so operators can see
87
+ * exactly what value an application is serving and where it came from. This
88
+ * is a read-only snapshot — it does not contact the server or evaluate
89
+ * targeting rules for a specific user.
90
+ */
91
+ inspect(): FlagInspection[];
71
92
  register<T extends (...args: any[]) => any>(operation: string, handler: T, flagKey?: string): void;
72
93
  dispatch<T extends (...args: any[]) => any>(operation: string, _context?: EvaluationContext): T;
73
94
  private evaluate;
95
+ /**
96
+ * Load offline default values from a flag-export file. When the live API
97
+ * is unreachable and the cache has no entry, the SDK falls back to these
98
+ * defaults for {@link boolValue} / {@link stringValue} / {@link intValue}
99
+ * / {@link jsonValue} calls.
100
+ *
101
+ * Accepts the JSON (default) or YAML format produced by
102
+ * `deploysentry flags export`.
103
+ *
104
+ * Environment matching: the SDK matches the configured `environment`
105
+ * option against the export's `environments[]` first by UUID, then by
106
+ * name (case-insensitive). When a match is found, that env's per-flag
107
+ * `{enabled, value}` overrides the flag's global `default_value`.
108
+ *
109
+ * @example
110
+ * await client.loadDefaultsFromFile('flags.json');
111
+ */
112
+ loadDefaultsFromFile(path: string): Promise<void>;
113
+ /**
114
+ * Synchronous variant of {@link loadDefaultsFromFile} that accepts an
115
+ * already-parsed object (or a JSON string).
116
+ */
117
+ loadDefaults(payload: unknown): void;
74
118
  private fetchAllFlags;
119
+ private fetchFlag;
75
120
  private post;
76
121
  private request;
77
122
  private authHeaders;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,IAAI,EACJ,YAAY,EAEb,MAAM,SAAS,CAAC;AA2CjB;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6C;IAClE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;IAEvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,QAAQ,CAA0C;gBAE9C,OAAO,EAAE,aAAa;IAuBlC;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA8CjC,0DAA0D;IAC1D,KAAK,IAAI,IAAI;IAOb;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAQrC,+DAA+D;IAC/D,IAAI,aAAa,IAAI,OAAO,CAE3B;IAMD,oCAAoC;IAC9B,SAAS,CACb,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,OAAO,EACrB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,OAAO,CAAC;IAcnB,mCAAmC;IAC7B,WAAW,CACf,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,MAAM,CAAC;IAKlB,qCAAqC;IAC/B,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,MAAM,CAAC;IAOlB,oEAAoE;IAC9D,SAAS,CAAC,CAAC,GAAG,OAAO,EACzB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,CAAC,CAAC;IASb;;;OAGG;IACG,MAAM,CACV,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,gBAAgB,CAAC;IA2B5B,6DAA6D;IAC7D,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,EAAE;IAI/C,gEAAgE;IAChE,YAAY,IAAI,IAAI,EAAE;IAOtB,oDAAoD;IACpD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IAKjC,2DAA2D;IAC3D,QAAQ,IAAI,IAAI,EAAE;IAQlB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,MAAM,GACf,IAAI;IAeP,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EACxC,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,CAAC;YA0BU,QAAQ;YAkCR,aAAa;YAQb,IAAI;YAIJ,OAAO;IAgCrB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,aAAa;IAoBrB,OAAO,CAAC,YAAY;CAmBrB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
@@ -5,13 +5,32 @@ const cache_1 = require("./cache");
5
5
  const streaming_1 = require("./streaming");
6
6
  const file_loader_1 = require("./file-loader");
7
7
  const local_evaluator_1 = require("./local-evaluator");
8
+ const defaults_1 = require("./defaults");
9
+ const status_reporter_1 = require("./status-reporter");
8
10
  const DEFAULT_BASE_URL = 'https://api.dr-sentry.com';
11
+ /** Parse a string value into its typed representation based on flag_type. */
12
+ function parseValue(raw, flagType) {
13
+ if (!raw && raw !== '0')
14
+ return undefined;
15
+ switch (flagType) {
16
+ case 'boolean': return raw === 'true';
17
+ case 'integer':
18
+ case 'number': return Number(raw);
19
+ case 'json': try {
20
+ return JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return raw;
24
+ }
25
+ default: return raw;
26
+ }
27
+ }
9
28
  /** Map the API's snake_case flag response to the SDK's Flag type. */
10
29
  function mapRawFlag(raw) {
11
30
  return {
12
31
  key: raw.key,
13
32
  enabled: raw.enabled,
14
- value: raw.default_value,
33
+ value: parseValue(raw.default_value, raw.flag_type),
15
34
  metadata: {
16
35
  category: raw.category,
17
36
  purpose: raw.purpose ?? '',
@@ -51,11 +70,18 @@ class DeploySentryClient {
51
70
  sessionId;
52
71
  mode;
53
72
  flagFilePath;
73
+ onFlagChange;
54
74
  cache;
55
75
  streamClient = null;
56
76
  _initialized = false;
57
77
  flagConfig = null;
78
+ offlineDefaults = null;
79
+ offlineDefaultsEnvUUID = '';
80
+ flagConfigLoadedAt;
81
+ offlineDefaultsLoadedAt;
58
82
  registry = new Map();
83
+ statusReporter = null;
84
+ statusReporterOptions;
59
85
  constructor(options) {
60
86
  if (!options.apiKey)
61
87
  throw new Error('apiKey is required');
@@ -74,7 +100,9 @@ class DeploySentryClient {
74
100
  this.sessionId = options.sessionId;
75
101
  this.mode = options.mode ?? 'server';
76
102
  this.flagFilePath = options.flagFilePath;
103
+ this.onFlagChange = options.onFlagChange;
77
104
  this.cache = new cache_1.FlagCache(options.cacheTimeout ?? DEFAULT_CACHE_TIMEOUT_MS);
105
+ this.statusReporterOptions = options;
78
106
  }
79
107
  // ---------------------------------------------------------------------------
80
108
  // Lifecycle
@@ -86,6 +114,7 @@ class DeploySentryClient {
86
114
  async initialize() {
87
115
  if (this.mode === 'file') {
88
116
  this.flagConfig = (0, file_loader_1.loadFlagConfig)(this.flagFilePath);
117
+ this.flagConfigLoadedAt = new Date().toISOString();
89
118
  this._initialized = true;
90
119
  return;
91
120
  }
@@ -97,13 +126,21 @@ class DeploySentryClient {
97
126
  // Fetch all flags for the project so the cache is warm.
98
127
  const flags = await this.fetchAllFlags();
99
128
  this.cache.setMany(flags);
100
- // Start streaming updates.
129
+ // Start streaming updates. Each SSE event identifies the changed flag
130
+ // by ID — fetch just that flag to get the authoritative state.
101
131
  this.streamClient = new streaming_1.FlagStreamClient({
102
132
  url: `${this.baseURL}/api/v1/flags/stream?project_id=${enc(this.project)}&environment_id=${enc(this.environment)}&application=${enc(this.application)}`,
103
133
  headers: this.authHeaders(),
104
- onChange: () => {
105
- this.fetchAllFlags()
106
- .then((flags) => this.cache.setMany(flags))
134
+ onChange: (change) => {
135
+ if (!change.flagId)
136
+ return;
137
+ this.fetchFlag(change.flagId)
138
+ .then((flag) => {
139
+ if (flag) {
140
+ this.cache.set(flag);
141
+ this.onFlagChange?.([flag]);
142
+ }
143
+ })
107
144
  .catch(() => { }); // stale cache still serves
108
145
  },
109
146
  onError: (err) => {
@@ -114,11 +151,13 @@ class DeploySentryClient {
114
151
  // Fire-and-forget; the stream reconnects automatically.
115
152
  this.streamClient.connect();
116
153
  this._initialized = true;
154
+ this.startStatusReporter();
117
155
  }
118
156
  catch (err) {
119
157
  if (this.mode === 'server-with-fallback') {
120
158
  console.warn('[DeploySentry] Server unavailable, falling back to flag config file');
121
159
  this.flagConfig = (0, file_loader_1.loadFlagConfig)(this.flagFilePath);
160
+ this.flagConfigLoadedAt = new Date().toISOString();
122
161
  this._initialized = true;
123
162
  return;
124
163
  }
@@ -129,9 +168,32 @@ class DeploySentryClient {
129
168
  close() {
130
169
  this.streamClient?.close();
131
170
  this.streamClient = null;
171
+ this.statusReporter?.stop();
172
+ this.statusReporter = null;
132
173
  this.cache.clear();
133
174
  this._initialized = false;
134
175
  }
176
+ startStatusReporter() {
177
+ const o = this.statusReporterOptions;
178
+ if (!o.reportStatus)
179
+ return;
180
+ if (!o.applicationId) {
181
+ console.warn('[DeploySentry] reportStatus=true but applicationId is not set; status reporter disabled');
182
+ return;
183
+ }
184
+ this.statusReporter = new status_reporter_1.StatusReporter({
185
+ baseURL: this.baseURL,
186
+ apiKey: this.apiKey,
187
+ applicationId: o.applicationId,
188
+ intervalMs: o.reportStatusIntervalMs,
189
+ version: o.reportStatusVersion,
190
+ commitSha: o.reportStatusCommitSha,
191
+ deploySlot: o.reportStatusDeploySlot,
192
+ tags: o.reportStatusTags,
193
+ healthProvider: o.reportStatusHealthProvider,
194
+ });
195
+ this.statusReporter.start();
196
+ }
135
197
  /**
136
198
  * Clear the local cache and re-fetch all flags from the server.
137
199
  * Useful when session state may have changed and the client needs
@@ -234,6 +296,72 @@ class DeploySentryClient {
234
296
  allFlags() {
235
297
  return this.cache.getAll();
236
298
  }
299
+ /**
300
+ * Return a provenance-tagged view of every flag the SDK currently knows
301
+ * about, drawn from all sources (live server cache, offline file config,
302
+ * and offline defaults). Each entry reports the resolved value, its type,
303
+ * the {@link FlagSource | source} it came from, an evaluation reason code,
304
+ * and when it was last fetched / loaded.
305
+ *
306
+ * Intended for wiring into an admin or support panel so operators can see
307
+ * exactly what value an application is serving and where it came from. This
308
+ * is a read-only snapshot — it does not contact the server or evaluate
309
+ * targeting rules for a specific user.
310
+ */
311
+ inspect() {
312
+ const byKey = new Map();
313
+ // 1) Server cache (live + stale). Highest-fidelity source; recorded first.
314
+ for (const e of this.cache.entries()) {
315
+ byKey.set(e.flag.key, {
316
+ key: e.flag.key,
317
+ value: e.flag.value,
318
+ type: typeOfValue(e.flag.value),
319
+ enabled: e.flag.enabled,
320
+ source: e.stale ? 'cache' : 'server',
321
+ reason: e.stale ? 'CACHE' : 'LIVE',
322
+ fetchedAt: new Date(e.storedAt).toISOString(),
323
+ stale: e.stale,
324
+ });
325
+ }
326
+ // 2) Offline file config (mode 'file' or fallback). Only fills gaps.
327
+ if (this.flagConfig) {
328
+ for (const f of this.flagConfig.flags) {
329
+ if (byKey.has(f.key))
330
+ continue;
331
+ const result = (0, local_evaluator_1.evaluateLocal)(this.flagConfig, this.environment, f.key);
332
+ const envState = f.environments[this.environment];
333
+ byKey.set(f.key, {
334
+ key: f.key,
335
+ value: parseValue(result.value, f.flag_type),
336
+ type: typeOfFlagType(f.flag_type),
337
+ enabled: envState?.enabled ?? false,
338
+ source: 'file',
339
+ reason: 'OFFLINE_FILE',
340
+ fetchedAt: this.flagConfigLoadedAt,
341
+ stale: false,
342
+ });
343
+ }
344
+ }
345
+ // 3) Offline defaults loaded via loadDefaults*(). Only fills gaps.
346
+ if (this.offlineDefaults) {
347
+ for (const [key, def] of this.offlineDefaults) {
348
+ if (byKey.has(key))
349
+ continue;
350
+ const resolved = (0, defaults_1.lookupOfflineDefault)(this.offlineDefaults, this.offlineDefaultsEnvUUID, key);
351
+ byKey.set(key, {
352
+ key,
353
+ value: resolved ? parseValue(resolved.value, def.flagType) : undefined,
354
+ type: typeOfFlagType(def.flagType),
355
+ enabled: resolved?.enabled ?? false,
356
+ source: 'file',
357
+ reason: 'OFFLINE_DEFAULT',
358
+ fetchedAt: this.offlineDefaultsLoadedAt,
359
+ stale: false,
360
+ });
361
+ }
362
+ }
363
+ return [...byKey.values()];
364
+ }
237
365
  // ---------------------------------------------------------------------------
238
366
  // Register / Dispatch
239
367
  // ---------------------------------------------------------------------------
@@ -297,14 +425,78 @@ class DeploySentryClient {
297
425
  return result.value;
298
426
  }
299
427
  catch {
300
- // Fall back to the cached flag value.
428
+ // 1) Cached value (from a prior live call) wins over offline defaults.
301
429
  const cached = this.cache.get(key);
302
- return cached?.value ?? defaultValue;
430
+ if (cached?.value !== undefined)
431
+ return cached.value;
432
+ // 2) Offline defaults loaded via loadDefaults*().
433
+ const offline = this.offlineDefaults
434
+ ? (0, defaults_1.lookupOfflineDefault)(this.offlineDefaults, this.offlineDefaultsEnvUUID, key)
435
+ : undefined;
436
+ if (offline) {
437
+ const requested = inferRequestedType(defaultValue);
438
+ const coerced = (0, defaults_1.coerceOfflineValue)(offline.value, requested);
439
+ if (coerced !== undefined)
440
+ return coerced;
441
+ }
442
+ return defaultValue;
303
443
  }
304
444
  }
445
+ // ---------------------------------------------------------------------------
446
+ // Offline defaults
447
+ // ---------------------------------------------------------------------------
448
+ /**
449
+ * Load offline default values from a flag-export file. When the live API
450
+ * is unreachable and the cache has no entry, the SDK falls back to these
451
+ * defaults for {@link boolValue} / {@link stringValue} / {@link intValue}
452
+ * / {@link jsonValue} calls.
453
+ *
454
+ * Accepts the JSON (default) or YAML format produced by
455
+ * `deploysentry flags export`.
456
+ *
457
+ * Environment matching: the SDK matches the configured `environment`
458
+ * option against the export's `environments[]` first by UUID, then by
459
+ * name (case-insensitive). When a match is found, that env's per-flag
460
+ * `{enabled, value}` overrides the flag's global `default_value`.
461
+ *
462
+ * @example
463
+ * await client.loadDefaultsFromFile('flags.json');
464
+ */
465
+ async loadDefaultsFromFile(path) {
466
+ const payload = await (0, defaults_1.readDefaultsFile)(path);
467
+ this.loadDefaults(payload);
468
+ }
469
+ /**
470
+ * Synchronous variant of {@link loadDefaultsFromFile} that accepts an
471
+ * already-parsed object (or a JSON string).
472
+ */
473
+ loadDefaults(payload) {
474
+ const data = typeof payload === 'string' ? JSON.parse(payload) : payload;
475
+ const { defaults, envUUID } = (0, defaults_1.parseDefaults)(data, this.environment);
476
+ this.offlineDefaults = defaults;
477
+ this.offlineDefaultsEnvUUID = envUUID;
478
+ this.offlineDefaultsLoadedAt = new Date().toISOString();
479
+ }
305
480
  async fetchAllFlags() {
306
- const response = await this.request('GET', `/api/v1/flags?project_id=${enc(this.project)}&application=${enc(this.application)}`);
307
- return (response.flags ?? []).map(mapRawFlag);
481
+ const pageSize = 500;
482
+ const flags = [];
483
+ let offset = 0;
484
+ for (;;) {
485
+ const response = await this.request('GET', `/api/v1/flags?project_id=${enc(this.project)}&application=${enc(this.application)}&environment_id=${enc(this.environment)}&limit=${pageSize}&offset=${offset}`);
486
+ const page = Array.isArray(response) ? response : (response.flags ?? []);
487
+ flags.push(...page);
488
+ const total = Array.isArray(response) ? undefined : response.total_count;
489
+ if (Array.isArray(response) || page.length === 0 || total === undefined || flags.length >= total)
490
+ break;
491
+ offset += page.length;
492
+ }
493
+ return flags.map(mapRawFlag);
494
+ }
495
+ async fetchFlag(flagId) {
496
+ const raw = await this.request('GET', `/api/v1/flags/${enc(flagId)}?environment_id=${enc(this.environment)}`);
497
+ if (!raw?.key)
498
+ return null;
499
+ return mapRawFlag(raw);
308
500
  }
309
501
  async post(path, body) {
310
502
  return this.request('POST', path, body);
@@ -380,4 +572,39 @@ exports.DeploySentryClient = DeploySentryClient;
380
572
  function enc(value) {
381
573
  return encodeURIComponent(value);
382
574
  }
575
+ /** Map a flag_type string to the inspection `type` field. */
576
+ function typeOfFlagType(flagType) {
577
+ switch (flagType) {
578
+ case 'boolean': return 'boolean';
579
+ case 'integer': return 'integer';
580
+ case 'number': return 'number';
581
+ case 'json': return 'json';
582
+ case 'string': return 'string';
583
+ default: return 'unknown';
584
+ }
585
+ }
586
+ /** Infer the inspection `type` field from an already-parsed value. */
587
+ function typeOfValue(value) {
588
+ switch (typeof value) {
589
+ case 'boolean': return 'boolean';
590
+ case 'number': return Number.isInteger(value) ? 'integer' : 'number';
591
+ case 'string': return 'string';
592
+ case 'object': return value === null ? 'unknown' : 'json';
593
+ default: return 'unknown';
594
+ }
595
+ }
596
+ /**
597
+ * Infer which coercion to apply to an offline-default raw value based on
598
+ * the caller's `defaultValue` type. Falls back to 'json' for objects /
599
+ * arrays so JSON-typed flags round-trip correctly.
600
+ */
601
+ function inferRequestedType(defaultValue) {
602
+ if (typeof defaultValue === 'boolean')
603
+ return 'bool';
604
+ if (typeof defaultValue === 'string')
605
+ return 'string';
606
+ if (typeof defaultValue === 'number')
607
+ return 'number';
608
+ return 'json';
609
+ }
383
610
  //# sourceMappingURL=client.js.map