codex-usage-analyzer 0.1.0 → 0.2.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.
package/src/cli.js CHANGED
@@ -1,75 +1,88 @@
1
- import {
2
- analyzeUsage,
3
- createSampleUsageSnapshotV2
4
- } from "./analyze.js";
1
+ import { readAccountUsage } from "./account-usage.js";
2
+ import { CodexUsageError } from "./errors.js";
3
+ import { formatAccountUsage } from "./format-account-usage.js";
4
+
5
+ export const PACKAGE_NAME = "codex-usage-analyzer";
6
+ export const PACKAGE_VERSION = "0.2.0";
5
7
 
6
8
  const USAGE = [
9
+ "codex-usage-analyzer - Read your Codex account usage",
10
+ "",
7
11
  "Usage:",
8
- " codex-usage-analyzer analyze --json [--codex-home <path>]",
9
- " codex-usage-analyzer analyze --json --fixture-sample"
12
+ " codex-usage-analyzer [usage] [--json]",
13
+ " codex-usage-analyzer [usage] --help",
14
+ " codex-usage-analyzer --version"
10
15
  ].join("\n");
11
16
 
12
- export async function runCli(argv, io = {}) {
17
+ export async function runCli(argv, io = {}, dependencies = {}) {
13
18
  const stdout = io.stdout ?? process.stdout;
14
19
  const stderr = io.stderr ?? process.stderr;
20
+ const parsed = parseArguments(argv);
15
21
 
16
- if (argv.includes("--help") || argv.includes("-h")) {
22
+ if (parsed.action === "help") {
17
23
  stdout.write(`${USAGE}\n`);
18
24
  return 0;
19
25
  }
20
26
 
21
- const [command, ...flags] = argv;
22
-
23
- if (command !== "analyze") {
24
- stderr.write(`${USAGE}\n`);
25
- return 1;
27
+ if (parsed.action === "version") {
28
+ stdout.write(`${PACKAGE_VERSION}\n`);
29
+ return 0;
26
30
  }
27
31
 
28
- const parsedFlags = parseAnalyzeFlags(flags);
29
- if (!parsedFlags.ok) {
32
+ if (parsed.action === "invalid") {
30
33
  stderr.write(`${USAGE}\n`);
31
34
  return 1;
32
35
  }
33
36
 
34
- const snapshot = parsedFlags.fixtureSample
35
- ? createSampleUsageSnapshotV2()
36
- : await analyzeUsage({
37
- codexHome: parsedFlags.codexHome
38
- });
39
- stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
40
- return 0;
41
- }
37
+ const readUsage = dependencies.readAccountUsage ?? readAccountUsage;
38
+ const formatUsage = dependencies.formatAccountUsage ?? formatAccountUsage;
42
39
 
43
- function parseAnalyzeFlags(flags) {
44
- const parsed = {
45
- codexHome: null,
46
- fixtureSample: false,
47
- hasJson: false,
48
- ok: true
49
- };
50
-
51
- for (let index = 0; index < flags.length; index += 1) {
52
- const flag = flags[index];
53
-
54
- if (flag === "--json") {
55
- parsed.hasJson = true;
56
- } else if (flag === "--fixture-sample") {
57
- parsed.fixtureSample = true;
58
- } else if (flag === "--codex-home") {
59
- const value = flags[index + 1];
60
- if (value === undefined || value.startsWith("--")) {
61
- return { ok: false };
62
- }
63
-
64
- parsed.codexHome = value;
65
- index += 1;
40
+ try {
41
+ const usage = await readUsage();
42
+ const output = parsed.json
43
+ ? JSON.stringify(usage, null, 2)
44
+ : formatUsage(usage);
45
+ stdout.write(`${output}\n`);
46
+ return 0;
47
+ } catch (error) {
48
+ if (error instanceof CodexUsageError) {
49
+ stderr.write(`${PACKAGE_NAME}: ${error.message} [${error.code}]\n`);
66
50
  } else {
67
- return { ok: false };
51
+ stderr.write(`${PACKAGE_NAME}: Unexpected failure. [UNEXPECTED_ERROR]\n`);
68
52
  }
53
+
54
+ return 1;
55
+ }
56
+ }
57
+
58
+ function parseArguments(argv) {
59
+ const args = [...argv];
60
+
61
+ if (args[0] === "usage") {
62
+ args.shift();
63
+ } else if (args[0] !== undefined && !args[0].startsWith("-")) {
64
+ return { action: "invalid" };
65
+ }
66
+
67
+ if (args.length === 0) {
68
+ return { action: "usage", json: false };
69
+ }
70
+
71
+ if (args.length !== 1) {
72
+ return { action: "invalid" };
73
+ }
74
+
75
+ if (args[0] === "--json") {
76
+ return { action: "usage", json: true };
77
+ }
78
+
79
+ if (args[0] === "--help" || args[0] === "-h") {
80
+ return { action: "help" };
81
+ }
82
+
83
+ if (args[0] === "--version" || args[0] === "-v") {
84
+ return { action: "version" };
69
85
  }
70
86
 
71
- return {
72
- ...parsed,
73
- ok: parsed.hasJson
74
- };
87
+ return { action: "invalid" };
75
88
  }
package/src/errors.js ADDED
@@ -0,0 +1,46 @@
1
+ export const CODEX_USAGE_ERROR_CODES = Object.freeze({
2
+ INVALID_TIMEOUT: "INVALID_TIMEOUT",
3
+ CODEX_NOT_FOUND: "CODEX_NOT_FOUND",
4
+ APP_SERVER_START_FAILED: "APP_SERVER_START_FAILED",
5
+ APP_SERVER_EXITED: "APP_SERVER_EXITED",
6
+ APP_SERVER_TIMEOUT: "APP_SERVER_TIMEOUT",
7
+ APP_SERVER_PROTOCOL_ERROR: "APP_SERVER_PROTOCOL_ERROR",
8
+ APP_SERVER_RPC_ERROR: "APP_SERVER_RPC_ERROR",
9
+ INVALID_ACCOUNT_USAGE_RESPONSE: "INVALID_ACCOUNT_USAGE_RESPONSE"
10
+ });
11
+
12
+ const ERROR_MESSAGES = Object.freeze({
13
+ [CODEX_USAGE_ERROR_CODES.INVALID_TIMEOUT]:
14
+ "timeoutMs must be an integer between 1 and 120000.",
15
+ [CODEX_USAGE_ERROR_CODES.CODEX_NOT_FOUND]:
16
+ "Codex CLI was not found. Install or update Codex, then sign in with ChatGPT.",
17
+ [CODEX_USAGE_ERROR_CODES.APP_SERVER_START_FAILED]:
18
+ "Codex app-server could not be started.",
19
+ [CODEX_USAGE_ERROR_CODES.APP_SERVER_EXITED]:
20
+ "Codex app-server exited before returning account usage.",
21
+ [CODEX_USAGE_ERROR_CODES.APP_SERVER_TIMEOUT]:
22
+ "Codex app-server timed out before returning account usage.",
23
+ [CODEX_USAGE_ERROR_CODES.APP_SERVER_PROTOCOL_ERROR]:
24
+ "Codex app-server returned an invalid protocol message.",
25
+ [CODEX_USAGE_ERROR_CODES.APP_SERVER_RPC_ERROR]:
26
+ "Codex app-server could not read account usage. Check your Codex version and ChatGPT sign-in.",
27
+ [CODEX_USAGE_ERROR_CODES.INVALID_ACCOUNT_USAGE_RESPONSE]:
28
+ "Codex app-server returned an invalid account usage response."
29
+ });
30
+
31
+ export class CodexUsageError extends Error {
32
+ constructor(code, options = {}) {
33
+ const message = ERROR_MESSAGES[code];
34
+ if (message === undefined) {
35
+ throw new TypeError("Unknown Codex usage error code");
36
+ }
37
+
38
+ super(message);
39
+ this.name = "CodexUsageError";
40
+ this.code = code;
41
+
42
+ if (Number.isInteger(options.rpcCode)) {
43
+ this.rpcCode = options.rpcCode;
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,81 @@
1
+ const UNAVAILABLE = "Unavailable";
2
+
3
+ export function formatAccountUsage(usage) {
4
+ const rows = [
5
+ ["Lifetime tokens", formatCompactInteger(usage.summary.lifetimeTokens)],
6
+ ["Peak daily tokens", formatCompactInteger(usage.summary.peakDailyTokens)],
7
+ ["Longest turn", formatDuration(usage.summary.longestRunningTurnSec)],
8
+ ["Current streak", formatDays(usage.summary.currentStreakDays)],
9
+ ["Longest streak", formatDays(usage.summary.longestStreakDays)],
10
+ ["Daily buckets", formatBucketCount(usage.dailyUsageBuckets)]
11
+ ];
12
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
13
+
14
+ return [
15
+ "Codex account usage",
16
+ "",
17
+ ...rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`),
18
+ "",
19
+ `Captured at ${usage.capturedAt}`
20
+ ].join("\n");
21
+ }
22
+
23
+ function formatCompactInteger(value) {
24
+ if (value === null) return UNAVAILABLE;
25
+
26
+ const units = [
27
+ [1_000_000_000_000, "T"],
28
+ [1_000_000_000, "B"],
29
+ [1_000_000, "M"],
30
+ [1_000, "K"]
31
+ ];
32
+
33
+ for (const [divisor, suffix] of units) {
34
+ if (value >= divisor) {
35
+ const scaled = value / divisor;
36
+ return `${trimTrailingZeros(scaled.toFixed(2))}${suffix}`;
37
+ }
38
+ }
39
+
40
+ return String(value);
41
+ }
42
+
43
+ function formatDuration(value) {
44
+ if (value === null) return UNAVAILABLE;
45
+ if (value === 0) return "0s";
46
+
47
+ let remaining = value;
48
+ const units = [
49
+ [86_400, "d"],
50
+ [3_600, "h"],
51
+ [60, "m"],
52
+ [1, "s"]
53
+ ];
54
+ const parts = [];
55
+
56
+ for (const [seconds, suffix] of units) {
57
+ const amount = Math.floor(remaining / seconds);
58
+ if (amount > 0) {
59
+ parts.push(`${amount}${suffix}`);
60
+ remaining -= amount * seconds;
61
+ }
62
+
63
+ if (parts.length === 2) break;
64
+ }
65
+
66
+ return parts.join(" ");
67
+ }
68
+
69
+ function formatDays(value) {
70
+ if (value === null) return UNAVAILABLE;
71
+ return `${value} ${value === 1 ? "day" : "days"}`;
72
+ }
73
+
74
+ function formatBucketCount(value) {
75
+ if (value === null) return UNAVAILABLE;
76
+ return `${value.length} ${value.length === 1 ? "day" : "days"}`;
77
+ }
78
+
79
+ function trimTrailingZeros(value) {
80
+ return value.replace(/\.0+$/u, "").replace(/(\.\d*?)0+$/u, "$1");
81
+ }
package/src/index.d.ts CHANGED
@@ -1,23 +1,63 @@
1
- import type { UsageSnapshotV2 } from "./snapshot/v2-types.js";
2
-
3
- export * from "./snapshot/v2-types.js";
4
-
5
- export declare const ANALYZER_NAME: "codex-usage-analyzer";
6
- export declare const ANALYZER_VERSION: string;
7
-
8
- export interface AnalyzeUsageOptions {
9
- /**
10
- * Capture timestamp for the production snapshot. Defaults to the analyzer run time.
11
- */
12
- capturedAt?: string | Date | null;
13
- /**
14
- * Local Codex home root for parser source discovery.
15
- */
16
- codexHome?: string | null;
17
- /**
18
- * Deterministic clock value for date and streak calculations in parser tests.
19
- */
20
- now?: string | Date | null;
1
+ export declare const PACKAGE_NAME: "codex-usage-analyzer";
2
+ export declare const PACKAGE_VERSION: "0.2.0";
3
+ export declare const ACCOUNT_USAGE_CONTRACT_VERSION: 1;
4
+ export declare const ACCOUNT_USAGE_SUMMARY_FIELDS: readonly [
5
+ "lifetimeTokens",
6
+ "peakDailyTokens",
7
+ "longestRunningTurnSec",
8
+ "currentStreakDays",
9
+ "longestStreakDays"
10
+ ];
11
+
12
+ export interface AccountUsageSummary {
13
+ lifetimeTokens: number | null;
14
+ peakDailyTokens: number | null;
15
+ longestRunningTurnSec: number | null;
16
+ currentStreakDays: number | null;
17
+ longestStreakDays: number | null;
18
+ }
19
+
20
+ export interface AccountUsageDailyBucket {
21
+ startDate: string;
22
+ tokens: number;
23
+ }
24
+
25
+ export interface AccountUsageDocument {
26
+ contractVersion: 1;
27
+ capturedAt: string;
28
+ summary: AccountUsageSummary;
29
+ dailyUsageBuckets: AccountUsageDailyBucket[] | null;
30
+ }
31
+
32
+ export interface ReadAccountUsageOptions {
33
+ timeoutMs?: number;
34
+ }
35
+
36
+ export type CodexUsageErrorCode =
37
+ | "INVALID_TIMEOUT"
38
+ | "CODEX_NOT_FOUND"
39
+ | "APP_SERVER_START_FAILED"
40
+ | "APP_SERVER_EXITED"
41
+ | "APP_SERVER_TIMEOUT"
42
+ | "APP_SERVER_PROTOCOL_ERROR"
43
+ | "APP_SERVER_RPC_ERROR"
44
+ | "INVALID_ACCOUNT_USAGE_RESPONSE";
45
+
46
+ export declare const CODEX_USAGE_ERROR_CODES: Readonly<{
47
+ INVALID_TIMEOUT: "INVALID_TIMEOUT";
48
+ CODEX_NOT_FOUND: "CODEX_NOT_FOUND";
49
+ APP_SERVER_START_FAILED: "APP_SERVER_START_FAILED";
50
+ APP_SERVER_EXITED: "APP_SERVER_EXITED";
51
+ APP_SERVER_TIMEOUT: "APP_SERVER_TIMEOUT";
52
+ APP_SERVER_PROTOCOL_ERROR: "APP_SERVER_PROTOCOL_ERROR";
53
+ APP_SERVER_RPC_ERROR: "APP_SERVER_RPC_ERROR";
54
+ INVALID_ACCOUNT_USAGE_RESPONSE: "INVALID_ACCOUNT_USAGE_RESPONSE";
55
+ }>;
56
+
57
+ export declare class CodexUsageError extends Error {
58
+ readonly code: CodexUsageErrorCode;
59
+ readonly rpcCode?: number;
60
+ constructor(code: CodexUsageErrorCode, options?: { rpcCode?: number });
21
61
  }
22
62
 
23
63
  export interface CliIo {
@@ -29,18 +69,8 @@ export interface CliIo {
29
69
  };
30
70
  }
31
71
 
32
- /**
33
- * Analyze usage into a production UsageSnapshot v2.
34
- *
35
- * Local session JSONL sources, including allowlisted usage and skill/plugin
36
- * invocation events, are parsed when available. Unavailable fields are
37
- * represented with zero/null/empty values plus a namespaced diagnostic
38
- * extension. This function does not return the sample fixture.
39
- */
40
- export declare function analyzeUsage(options?: AnalyzeUsageOptions): Promise<UsageSnapshotV2>;
41
-
42
- export declare function createSampleUsageSnapshotV2(
43
- overrides?: Record<string, unknown>
44
- ): UsageSnapshotV2;
72
+ export declare function readAccountUsage(
73
+ options?: ReadAccountUsageOptions
74
+ ): Promise<AccountUsageDocument>;
45
75
 
46
76
  export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
package/src/index.js CHANGED
@@ -1,17 +1,13 @@
1
1
  export {
2
- ANALYZER_NAME,
3
- ANALYZER_VERSION,
4
- analyzeUsage,
5
- createSampleUsageSnapshotV2
6
- } from "./analyze.js";
2
+ ACCOUNT_USAGE_CONTRACT_VERSION,
3
+ ACCOUNT_USAGE_SUMMARY_FIELDS,
4
+ readAccountUsage
5
+ } from "./account-usage.js";
7
6
 
8
- export {
9
- USAGE_SNAPSHOT_V2_SCHEMA_VERSION,
10
- assertUsageSnapshotV2,
11
- isUsageSnapshotV2,
12
- validateUsageSnapshotV2
13
- } from "./snapshot/v2-schema.js";
14
-
15
- export { sampleUsageSnapshotV2 } from "./fixtures/sample-v2-snapshot.js";
7
+ export { CODEX_USAGE_ERROR_CODES, CodexUsageError } from "./errors.js";
16
8
 
17
- export { runCli } from "./cli.js";
9
+ export {
10
+ PACKAGE_NAME,
11
+ PACKAGE_VERSION,
12
+ runCli
13
+ } from "./cli.js";
package/src/analyze.js DELETED
@@ -1,239 +0,0 @@
1
- import { sampleUsageSnapshotV2 } from "./fixtures/sample-v2-snapshot.js";
2
- import { aggregateActivityFromCodexHome } from "./parser/activity-aggregate.js";
3
- import { aggregateCodexAssetsFromCodexHome } from "./parser/asset-aggregate.js";
4
- import { aggregateModelUsageFromCodexHome } from "./parser/model-aggregate.js";
5
- import { aggregateSkillPluginUsageFromCodexHome } from "./parser/skill-plugin-aggregate.js";
6
- import { aggregateTokenUsageFromCodexHome } from "./parser/token-aggregate.js";
7
- import {
8
- USAGE_SNAPSHOT_V2_SCHEMA_VERSION,
9
- assertUsageSnapshotV2
10
- } from "./snapshot/v2-schema.js";
11
-
12
- export const ANALYZER_NAME = "codex-usage-analyzer";
13
- export const ANALYZER_VERSION = "0.1.0";
14
-
15
- export async function analyzeUsage(options = {}) {
16
- const [
17
- usageAggregate,
18
- modelAggregate,
19
- activityAggregate,
20
- skillPluginAggregate,
21
- assetAggregate
22
- ] = await Promise.all([
23
- aggregateTokenUsageFromCodexHome(options),
24
- aggregateModelUsageFromCodexHome(options),
25
- aggregateActivityFromCodexHome(options),
26
- aggregateSkillPluginUsageFromCodexHome(options),
27
- aggregateCodexAssetsFromCodexHome(options)
28
- ]);
29
-
30
- const snapshot = createUnavailableUsageSnapshotV2({
31
- capturedAt: normalizeCapturedAt(options.capturedAt),
32
- producer: {
33
- name: ANALYZER_NAME,
34
- version: ANALYZER_VERSION
35
- }
36
- });
37
-
38
- if (usageAggregate.diagnostics.status === "ok") {
39
- snapshot.usage = usageAggregate.usage;
40
- }
41
-
42
- if (modelAggregate.diagnostics.status === "ok") {
43
- snapshot.models = modelAggregate.models;
44
- }
45
-
46
- if (activityAggregate.diagnostics.status === "ok") {
47
- snapshot.activity = activityAggregate.activity;
48
- }
49
-
50
- if (skillPluginAggregate.diagnostics.status === "ok") {
51
- snapshot.skills = skillPluginAggregate.skills;
52
- snapshot.plugins = skillPluginAggregate.plugins;
53
- }
54
-
55
- if (assetAggregate.diagnostics.status === "ok") {
56
- snapshot.codexAssets = assetAggregate.codexAssets;
57
- }
58
-
59
- snapshot.extensions["codexUsageAnalyzer.diagnostics"] = createAnalyzerDiagnostics({
60
- activity: activityAggregate,
61
- assets: assetAggregate,
62
- models: modelAggregate,
63
- skillPlugin: skillPluginAggregate,
64
- usage: usageAggregate
65
- });
66
-
67
- return assertUsageSnapshotV2(snapshot);
68
- }
69
-
70
- function createUnavailableUsageSnapshotV2(overrides = {}) {
71
- return mergeSnapshot({
72
- schemaVersion: USAGE_SNAPSHOT_V2_SCHEMA_VERSION,
73
- capturedAt: new Date().toISOString(),
74
- producer: {
75
- name: ANALYZER_NAME,
76
- version: ANALYZER_VERSION
77
- },
78
- usage: {
79
- totalTokens: 0,
80
- peakDailyTokens: null,
81
- tokenBreakdown: createUnavailableTokenBreakdown(),
82
- daily: []
83
- },
84
- models: {
85
- favoriteModel: null,
86
- items: []
87
- },
88
- activity: {
89
- longestTaskDurationMs: null,
90
- currentStreakDays: null,
91
- longestStreakDays: null,
92
- fastModePercent: null,
93
- reasoningEffort: null,
94
- reasoningEffortPercent: null,
95
- totalThreads: null
96
- },
97
- skills: {
98
- exploredCount: null,
99
- totalUsed: null,
100
- topSkills: []
101
- },
102
- plugins: {
103
- topPlugins: []
104
- },
105
- extensions: {
106
- "codexUsageAnalyzer.diagnostics": {
107
- status: "unavailable",
108
- reason: "local_sources_unavailable",
109
- unavailableFields: ["usage", "models", "activity", "skills", "plugins"]
110
- }
111
- }
112
- }, overrides);
113
- }
114
-
115
- function createUnavailableTokenBreakdown() {
116
- return {
117
- inputTokens: null,
118
- outputTokens: null,
119
- cacheReadTokens: null,
120
- cacheWriteTokens: null,
121
- reasoningTokens: null
122
- };
123
- }
124
-
125
- function createAnalyzerDiagnostics(aggregates) {
126
- const skillPluginDiagnostics = aggregates.skillPlugin.diagnostics;
127
- const diagnostics = {
128
- usage: aggregates.usage.diagnostics,
129
- models: aggregates.models.diagnostics,
130
- activity: aggregates.activity.diagnostics,
131
- skills: skillPluginDiagnostics,
132
- plugins: skillPluginDiagnostics,
133
- codexAssets: aggregates.assets.diagnostics
134
- };
135
-
136
- const parsedFields = [];
137
- const unavailableFields = [];
138
-
139
- for (const field of ["usage", "models", "activity"]) {
140
- if (diagnostics[field].status === "ok") {
141
- parsedFields.push(field);
142
- } else {
143
- unavailableFields.push(field);
144
- }
145
- }
146
-
147
- if (skillPluginDiagnostics.status === "ok") {
148
- parsedFields.push("skills", "plugins");
149
- } else {
150
- unavailableFields.push("skills", "plugins");
151
- }
152
-
153
- if (diagnostics.codexAssets.status === "ok") {
154
- parsedFields.push("codexAssets");
155
- } else {
156
- unavailableFields.push("codexAssets");
157
- }
158
-
159
- if (diagnostics.activity.status === "ok") {
160
- for (const field of diagnostics.activity.unavailableFields ?? []) {
161
- unavailableFields.push(`activity.${field}`);
162
- }
163
- }
164
-
165
- const status = parsedFields.length === 0
166
- ? "unavailable"
167
- : unavailableFields.length > 0
168
- ? "partial"
169
- : "ok";
170
-
171
- return {
172
- status,
173
- reason: getAnalyzerDiagnosticReason(status, diagnostics),
174
- parser: "session_jsonl",
175
- source: diagnostics.usage.source,
176
- profileComparison: {
177
- status: "not_performed",
178
- reason: "remote_profile_api_not_used",
179
- localStreakBasis: "session_jsonl_utc_dates",
180
- remoteProfileBasis: "codex_desktop_remote_profile_api",
181
- parity: "not_guaranteed"
182
- },
183
- parsedFields,
184
- unavailableFields,
185
- ...diagnostics
186
- };
187
- }
188
-
189
- function getAnalyzerDiagnosticReason(status, diagnostics) {
190
- if (status === "ok") {
191
- return null;
192
- }
193
-
194
- if (status === "partial") {
195
- return "local_sources_partially_available";
196
- }
197
-
198
- return diagnostics.usage.reason
199
- ?? diagnostics.models.reason
200
- ?? diagnostics.activity.reason
201
- ?? diagnostics.skills.reason
202
- ?? diagnostics.plugins.reason
203
- ?? diagnostics.codexAssets.reason
204
- ?? "local_sources_unavailable";
205
- }
206
-
207
- export function createSampleUsageSnapshotV2(overrides = {}) {
208
- return mergeSnapshot(structuredClone(sampleUsageSnapshotV2), overrides);
209
- }
210
-
211
- function normalizeCapturedAt(value) {
212
- if (value === undefined || value === null) {
213
- return new Date().toISOString();
214
- }
215
-
216
- const date = value instanceof Date ? value : new Date(value);
217
-
218
- if (Number.isNaN(date.getTime())) {
219
- throw new TypeError("capturedAt must be a valid date");
220
- }
221
-
222
- return date.toISOString();
223
- }
224
-
225
- function mergeSnapshot(target, source) {
226
- for (const [key, value] of Object.entries(source)) {
227
- if (isPlainObject(value) && isPlainObject(target[key])) {
228
- target[key] = mergeSnapshot(target[key], value);
229
- } else {
230
- target[key] = value;
231
- }
232
- }
233
-
234
- return target;
235
- }
236
-
237
- function isPlainObject(value) {
238
- return value !== null && typeof value === "object" && !Array.isArray(value);
239
- }