@sanity/sdk 2.5.0 → 2.7.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/dist/index.d.ts +429 -27
- package/dist/index.js +657 -266
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/_exports/index.ts +18 -3
- package/src/auth/authMode.test.ts +56 -0
- package/src/auth/authMode.ts +71 -0
- package/src/auth/authStore.test.ts +85 -4
- package/src/auth/authStore.ts +63 -125
- package/src/auth/authStrategy.ts +39 -0
- package/src/auth/dashboardAuth.ts +132 -0
- package/src/auth/standaloneAuth.ts +109 -0
- package/src/auth/studioAuth.ts +217 -0
- package/src/auth/studioModeAuth.test.ts +43 -1
- package/src/auth/studioModeAuth.ts +10 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
- package/src/client/clientStore.test.ts +45 -43
- package/src/client/clientStore.ts +23 -9
- package/src/config/loggingConfig.ts +149 -0
- package/src/config/sanityConfig.ts +82 -22
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.test.ts +38 -55
- package/src/projection/projectionQuery.ts +27 -31
- package/src/projection/projectionStore.test.ts +4 -4
- package/src/projection/projectionStore.ts +3 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/statusQuery.test.ts +35 -0
- package/src/projection/statusQuery.ts +71 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
- package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
- package/src/projection/types.ts +12 -0
- package/src/projection/util.ts +0 -1
- package/src/query/queryStore.test.ts +64 -0
- package/src/query/queryStore.ts +33 -11
- package/src/releases/getPerspectiveState.test.ts +17 -14
- package/src/releases/getPerspectiveState.ts +58 -38
- package/src/releases/releasesStore.test.ts +59 -61
- package/src/releases/releasesStore.ts +21 -35
- package/src/releases/utils/isReleasePerspective.ts +7 -0
- package/src/store/createActionBinder.test.ts +211 -1
- package/src/store/createActionBinder.ts +102 -13
- package/src/store/createSanityInstance.test.ts +85 -1
- package/src/store/createSanityInstance.ts +55 -4
- package/src/utils/logger-usage-example.md +141 -0
- package/src/utils/logger.test.ts +757 -0
- package/src/utils/logger.ts +537 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging infrastructure for the Sanity SDK
|
|
3
|
+
*
|
|
4
|
+
* Provides multi-level, namespace-based logging for both SDK users and maintainers.
|
|
5
|
+
* In production builds, all logging can be stripped via tree-shaking.
|
|
6
|
+
*
|
|
7
|
+
* @example SDK User
|
|
8
|
+
* ```ts
|
|
9
|
+
* import {configureLogging} from '@sanity/sdk'
|
|
10
|
+
*
|
|
11
|
+
* configureLogging({
|
|
12
|
+
* level: 'info',
|
|
13
|
+
* namespaces: ['auth', 'document']
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example SDK Maintainer
|
|
18
|
+
* ```ts
|
|
19
|
+
* configureLogging({
|
|
20
|
+
* level: 'trace',
|
|
21
|
+
* namespaces: ['*'],
|
|
22
|
+
* internal: true
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Log levels in order of verbosity (least to most)
|
|
29
|
+
* - error: Critical failures that prevent operation
|
|
30
|
+
* - warn: Issues that may cause problems but don't stop execution
|
|
31
|
+
* - info: High-level informational messages (SDK user level)
|
|
32
|
+
* - debug: Detailed debugging information (maintainer level)
|
|
33
|
+
* - trace: Very detailed tracing (maintainer level, includes RxJS streams)
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Log namespaces organize logs by functional domain
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* This is an extensible string type. As logging is added to more modules,
|
|
43
|
+
* additional namespaces will be recognized. Currently implemented namespaces
|
|
44
|
+
* will be documented as they are added.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export type LogNamespace = string
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for the logging system
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
export interface LoggerConfig {
|
|
54
|
+
/**
|
|
55
|
+
* Minimum log level to output
|
|
56
|
+
* @defaultValue 'warn'
|
|
57
|
+
*/
|
|
58
|
+
level?: LogLevel
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Namespaces to enable. Use ['*'] for all namespaces
|
|
62
|
+
* @defaultValue []
|
|
63
|
+
* @remarks
|
|
64
|
+
* Available namespaces depend on which modules have logging integrated.
|
|
65
|
+
* Check the SDK documentation for the current list of instrumented modules.
|
|
66
|
+
* @example ['auth', 'document']
|
|
67
|
+
*/
|
|
68
|
+
namespaces?: string[]
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Enable internal/maintainer-level logging
|
|
72
|
+
* Shows RxJS streams, store internals, etc.
|
|
73
|
+
* @defaultValue false
|
|
74
|
+
*/
|
|
75
|
+
internal?: boolean
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Custom log handler (for testing or custom output)
|
|
79
|
+
* @defaultValue console methods
|
|
80
|
+
*/
|
|
81
|
+
handler?: LogHandler
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Enable timestamps in log output
|
|
85
|
+
* @defaultValue true
|
|
86
|
+
*/
|
|
87
|
+
timestamps?: boolean
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Enable in production builds
|
|
91
|
+
* @defaultValue false
|
|
92
|
+
*/
|
|
93
|
+
enableInProduction?: boolean
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Custom log handler interface
|
|
98
|
+
*
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
export interface LogHandler {
|
|
102
|
+
error: (message: string, context?: LogContext) => void
|
|
103
|
+
warn: (message: string, context?: LogContext) => void
|
|
104
|
+
info: (message: string, context?: LogContext) => void
|
|
105
|
+
debug: (message: string, context?: LogContext) => void
|
|
106
|
+
trace: (message: string, context?: LogContext) => void
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Context object attached to log messages
|
|
111
|
+
*
|
|
112
|
+
* This interface allows you to attach arbitrary contextual data to log messages.
|
|
113
|
+
* The index signature `[key: string]: unknown` enables you to add any custom
|
|
114
|
+
* properties relevant to your log entry (e.g., `userId`, `documentId`, `action`, etc.).
|
|
115
|
+
*
|
|
116
|
+
* **Sensitive data sanitization:**
|
|
117
|
+
* Top-level keys containing sensitive names (`token`, `password`, `secret`, `apiKey`,
|
|
118
|
+
* `authorization`) are automatically redacted to `[REDACTED]` in log output.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* logger.info('User logged in', {
|
|
123
|
+
* userId: '123', // Custom context
|
|
124
|
+
* action: 'login', // Custom context
|
|
125
|
+
* token: 'secret' // Will be redacted to [REDACTED]
|
|
126
|
+
* })
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export interface LogContext {
|
|
132
|
+
/**
|
|
133
|
+
* Custom context properties that provide additional information about the log entry.
|
|
134
|
+
* Any key-value pairs can be added here (e.g., userId, documentId, requestId, etc.).
|
|
135
|
+
* Keys with sensitive names (token, password, secret, apiKey, authorization) are
|
|
136
|
+
* automatically sanitized.
|
|
137
|
+
*/
|
|
138
|
+
[key: string]: unknown
|
|
139
|
+
/** Error object if logging an error */
|
|
140
|
+
error?: Error | unknown
|
|
141
|
+
/** Duration in milliseconds for timed operations */
|
|
142
|
+
duration?: number
|
|
143
|
+
/** Stack trace for debugging */
|
|
144
|
+
stack?: string
|
|
145
|
+
/** Instance context (automatically added when available) */
|
|
146
|
+
instanceContext?: InstanceContext
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Instance context information automatically added to logs
|
|
151
|
+
* @internal
|
|
152
|
+
*/
|
|
153
|
+
export interface InstanceContext {
|
|
154
|
+
/** Unique instance ID */
|
|
155
|
+
instanceId?: string
|
|
156
|
+
/** Project ID */
|
|
157
|
+
projectId?: string
|
|
158
|
+
/** Dataset name */
|
|
159
|
+
dataset?: string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Logger instance for a specific namespace
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
export interface Logger {
|
|
167
|
+
readonly namespace: string
|
|
168
|
+
error: (message: string, context?: LogContext) => void
|
|
169
|
+
warn: (message: string, context?: LogContext) => void
|
|
170
|
+
info: (message: string, context?: LogContext) => void
|
|
171
|
+
debug: (message: string, context?: LogContext) => void
|
|
172
|
+
trace: (message: string, context?: LogContext) => void
|
|
173
|
+
/** Check if a log level is enabled (for performance-sensitive code) */
|
|
174
|
+
isLevelEnabled: (level: LogLevel) => boolean
|
|
175
|
+
/** Create a child logger with extended context */
|
|
176
|
+
child: (context: LogContext) => Logger
|
|
177
|
+
/** Get the instance context if available */
|
|
178
|
+
getInstanceContext: () => InstanceContext | undefined
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Log level priority (lower number = higher priority)
|
|
182
|
+
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
183
|
+
error: 0,
|
|
184
|
+
warn: 1,
|
|
185
|
+
info: 2,
|
|
186
|
+
debug: 3,
|
|
187
|
+
trace: 4,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Default logging configuration
|
|
191
|
+
const DEFAULT_CONFIG: Required<LoggerConfig> = {
|
|
192
|
+
level: 'warn',
|
|
193
|
+
namespaces: [],
|
|
194
|
+
internal: false,
|
|
195
|
+
timestamps: true,
|
|
196
|
+
enableInProduction: false,
|
|
197
|
+
handler: {
|
|
198
|
+
// eslint-disable-next-line no-console
|
|
199
|
+
error: console.error.bind(console),
|
|
200
|
+
// eslint-disable-next-line no-console
|
|
201
|
+
warn: console.warn.bind(console),
|
|
202
|
+
// eslint-disable-next-line no-console
|
|
203
|
+
info: console.info.bind(console),
|
|
204
|
+
// eslint-disable-next-line no-console
|
|
205
|
+
debug: console.debug.bind(console),
|
|
206
|
+
// eslint-disable-next-line no-console
|
|
207
|
+
trace: console.debug.bind(console), // trace uses console.debug
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse DEBUG environment variable for automatic logging configuration
|
|
213
|
+
*
|
|
214
|
+
* Supports patterns similar to Sanity CLI/Studio:
|
|
215
|
+
* - DEBUG=sanity:* (all namespaces, debug level)
|
|
216
|
+
* - DEBUG=sanity:auth,sanity:document (specific namespaces)
|
|
217
|
+
* - DEBUG=sanity:trace:* (all namespaces, trace level)
|
|
218
|
+
* - DEBUG=sanity:*:internal (enable internal logs)
|
|
219
|
+
*
|
|
220
|
+
* @internal
|
|
221
|
+
*/
|
|
222
|
+
export function parseDebugEnvVar(): Partial<LoggerConfig> | null {
|
|
223
|
+
if (typeof process === 'undefined' || !process.env?.['DEBUG']) {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const debug = process.env['DEBUG']
|
|
228
|
+
|
|
229
|
+
// Only process if it includes 'sanity'
|
|
230
|
+
if (!debug.includes('sanity')) {
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const config: Partial<LoggerConfig> = {}
|
|
235
|
+
|
|
236
|
+
// Parse level from pattern like "sanity:trace:*" or "sanity:debug:*"
|
|
237
|
+
const levelMatch = debug.match(/sanity:(trace|debug|info|warn|error):/)
|
|
238
|
+
const hasLevelSpecifier = !!levelMatch
|
|
239
|
+
if (levelMatch) {
|
|
240
|
+
config.level = levelMatch[1] as LogLevel
|
|
241
|
+
} else {
|
|
242
|
+
// Default to debug level if just "sanity:*" or "sanity:namespace"
|
|
243
|
+
config.level = 'debug'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Parse namespaces
|
|
247
|
+
if (debug === 'sanity') {
|
|
248
|
+
config.namespaces = ['*']
|
|
249
|
+
} else if (hasLevelSpecifier && debug.match(/sanity:(trace|debug|info|warn|error):\*/)) {
|
|
250
|
+
// Pattern like "sanity:trace:*" - wildcard after level
|
|
251
|
+
config.namespaces = ['*']
|
|
252
|
+
} else if (!hasLevelSpecifier && debug.includes('sanity:*')) {
|
|
253
|
+
// Pattern like "sanity:*" - wildcard without level
|
|
254
|
+
config.namespaces = ['*']
|
|
255
|
+
} else {
|
|
256
|
+
// Extract specific namespaces like "sanity:auth,sanity:document"
|
|
257
|
+
const namespaces = debug
|
|
258
|
+
.split(',')
|
|
259
|
+
.filter((s) => s.includes('sanity:'))
|
|
260
|
+
.map((s) => {
|
|
261
|
+
// Remove 'sanity:' prefix
|
|
262
|
+
const cleaned = s.replace(/^sanity:/, '')
|
|
263
|
+
// If there's a level specifier, skip it
|
|
264
|
+
if (hasLevelSpecifier && cleaned.match(/^(trace|debug|info|warn|error):/)) {
|
|
265
|
+
// Get the part after the level: "trace:auth" -> "auth"
|
|
266
|
+
return cleaned.split(':').slice(1).join(':')
|
|
267
|
+
}
|
|
268
|
+
// Otherwise get the first part: "auth:something" -> "auth"
|
|
269
|
+
return cleaned.split(':')[0]
|
|
270
|
+
})
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.filter((ns) => ns !== '*') // Filter out wildcards
|
|
273
|
+
|
|
274
|
+
if (namespaces.length > 0) {
|
|
275
|
+
config.namespaces = namespaces
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check for internal flag: DEBUG=sanity:*:internal
|
|
280
|
+
if (debug.includes(':internal')) {
|
|
281
|
+
config.internal = true
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return config
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Global configuration - initialized with env var settings if present
|
|
288
|
+
const envConfig = parseDebugEnvVar()
|
|
289
|
+
let globalConfig: Required<LoggerConfig> = {
|
|
290
|
+
...DEFAULT_CONFIG,
|
|
291
|
+
...(envConfig ?? {}),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Log that env var configuration was detected (only if DEBUG is set)
|
|
295
|
+
// Note: This runs at module initialization, difficult to test without complex module mocking
|
|
296
|
+
/* c8 ignore next 14 */
|
|
297
|
+
if (envConfig) {
|
|
298
|
+
const shouldLog =
|
|
299
|
+
['info', 'debug', 'trace'].includes(globalConfig.level) || globalConfig.level === 'warn'
|
|
300
|
+
if (shouldLog) {
|
|
301
|
+
// eslint-disable-next-line no-console
|
|
302
|
+
console.info(
|
|
303
|
+
`[${new Date().toISOString()}] [INFO] [sdk] Logging auto-configured from DEBUG environment variable`,
|
|
304
|
+
{
|
|
305
|
+
level: globalConfig.level,
|
|
306
|
+
namespaces: globalConfig.namespaces,
|
|
307
|
+
internal: globalConfig.internal,
|
|
308
|
+
source: 'env:DEBUG',
|
|
309
|
+
value: typeof process !== 'undefined' ? process.env?.['DEBUG'] : undefined,
|
|
310
|
+
},
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Configure the global logging system
|
|
317
|
+
* @public
|
|
318
|
+
*/
|
|
319
|
+
export function configureLogging(config: LoggerConfig): void {
|
|
320
|
+
globalConfig = {
|
|
321
|
+
...globalConfig,
|
|
322
|
+
...config,
|
|
323
|
+
handler: config.handler ?? globalConfig.handler,
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get the current logging configuration
|
|
329
|
+
* @internal
|
|
330
|
+
*/
|
|
331
|
+
export function getLoggingConfig(): Readonly<Required<LoggerConfig>> {
|
|
332
|
+
return globalConfig
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Reset logging to default configuration
|
|
337
|
+
* @internal
|
|
338
|
+
*/
|
|
339
|
+
export function resetLogging(): void {
|
|
340
|
+
globalConfig = {...DEFAULT_CONFIG}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Check if logging is enabled in the current environment
|
|
345
|
+
* @internal
|
|
346
|
+
*/
|
|
347
|
+
function isLoggingEnabled(): boolean {
|
|
348
|
+
// In production, only log if explicitly enabled
|
|
349
|
+
if (typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'production') {
|
|
350
|
+
return globalConfig.enableInProduction
|
|
351
|
+
}
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check if a namespace is enabled
|
|
357
|
+
* @internal
|
|
358
|
+
*/
|
|
359
|
+
function isNamespaceEnabled(namespace: string): boolean {
|
|
360
|
+
if (!isLoggingEnabled()) return false
|
|
361
|
+
if (globalConfig.namespaces.includes('*')) return true
|
|
362
|
+
return globalConfig.namespaces.includes(namespace)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if a log level is enabled
|
|
367
|
+
* @internal
|
|
368
|
+
*/
|
|
369
|
+
function isLevelEnabled(level: LogLevel): boolean {
|
|
370
|
+
if (!isLoggingEnabled()) return false
|
|
371
|
+
return LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[globalConfig.level]
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Format a log message with timestamp, namespace, and instance context
|
|
376
|
+
* @internal
|
|
377
|
+
*/
|
|
378
|
+
function formatMessage(
|
|
379
|
+
namespace: LogNamespace,
|
|
380
|
+
level: LogLevel,
|
|
381
|
+
message: string,
|
|
382
|
+
context?: LogContext,
|
|
383
|
+
): [string, LogContext | undefined] {
|
|
384
|
+
const parts: string[] = []
|
|
385
|
+
|
|
386
|
+
if (globalConfig.timestamps) {
|
|
387
|
+
const timestamp = new Date().toISOString()
|
|
388
|
+
parts.push(`[${timestamp}]`)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
parts.push(`[${level.toUpperCase()}]`)
|
|
392
|
+
parts.push(`[${namespace}]`)
|
|
393
|
+
|
|
394
|
+
// Add instance context if available
|
|
395
|
+
const instanceContext = context?.instanceContext
|
|
396
|
+
if (instanceContext) {
|
|
397
|
+
if (instanceContext.projectId) {
|
|
398
|
+
parts.push(`[project:${instanceContext.projectId}]`)
|
|
399
|
+
}
|
|
400
|
+
if (instanceContext.dataset) {
|
|
401
|
+
parts.push(`[dataset:${instanceContext.dataset}]`)
|
|
402
|
+
}
|
|
403
|
+
if (instanceContext.instanceId) {
|
|
404
|
+
parts.push(`[instance:${instanceContext.instanceId.slice(0, 8)}]`)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
parts.push(message)
|
|
409
|
+
|
|
410
|
+
return [parts.join(' '), context]
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Sanitize context for logging (remove sensitive data)
|
|
415
|
+
* @internal
|
|
416
|
+
* @remarks
|
|
417
|
+
* This performs shallow sanitization only - it redacts top-level keys that contain
|
|
418
|
+
* sensitive names (token, password, secret, apiKey, authorization).
|
|
419
|
+
* Nested sensitive data (e.g., `\{auth: \{token: 'secret'\}\}`) will NOT be redacted.
|
|
420
|
+
* If deep sanitization is needed in the future, this can be enhanced to recursively
|
|
421
|
+
* traverse nested objects.
|
|
422
|
+
*/
|
|
423
|
+
function sanitizeContext(context?: LogContext): LogContext | undefined {
|
|
424
|
+
if (!context || Object.keys(context).length === 0) return undefined
|
|
425
|
+
|
|
426
|
+
const sanitized = {...context}
|
|
427
|
+
|
|
428
|
+
// Remove or redact sensitive fields at the top level
|
|
429
|
+
const sensitiveKeys = ['token', 'password', 'secret', 'apiKey', 'authorization']
|
|
430
|
+
for (const key of Object.keys(sanitized)) {
|
|
431
|
+
if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) {
|
|
432
|
+
sanitized[key] = '[REDACTED]'
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return sanitized
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Create a logger for a specific namespace
|
|
441
|
+
* @param namespace - A string identifier for the logging domain (e.g., 'auth', 'document')
|
|
442
|
+
* @param baseContext - Optional base context to include in all log messages
|
|
443
|
+
* @remarks
|
|
444
|
+
* If baseContext includes an `instanceContext` property (with projectId, dataset, instanceId),
|
|
445
|
+
* it will be automatically formatted into the log output for easier debugging of
|
|
446
|
+
* multi-instance scenarios.
|
|
447
|
+
* @public
|
|
448
|
+
*/
|
|
449
|
+
export function createLogger(namespace: string, baseContext?: LogContext): Logger {
|
|
450
|
+
const logAtLevel = (level: LogLevel, message: string, context?: LogContext) => {
|
|
451
|
+
// Early return if namespace or level not enabled
|
|
452
|
+
if (!isNamespaceEnabled(namespace)) return
|
|
453
|
+
if (!isLevelEnabled(level)) return
|
|
454
|
+
|
|
455
|
+
// Skip internal logs if not enabled
|
|
456
|
+
if (context?.['internal'] && !globalConfig.internal) return
|
|
457
|
+
|
|
458
|
+
const mergedContext = {...baseContext, ...context}
|
|
459
|
+
const sanitized = sanitizeContext(mergedContext)
|
|
460
|
+
const [formatted, finalContext] = formatMessage(namespace, level, message, sanitized)
|
|
461
|
+
|
|
462
|
+
globalConfig.handler[level](formatted, finalContext)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const logger: Logger = {
|
|
466
|
+
namespace,
|
|
467
|
+
error: (message, context) => logAtLevel('error', message, context),
|
|
468
|
+
warn: (message, context) => logAtLevel('warn', message, context),
|
|
469
|
+
info: (message, context) => logAtLevel('info', message, context),
|
|
470
|
+
debug: (message, context) => logAtLevel('debug', message, context),
|
|
471
|
+
trace: (message, context) => logAtLevel('trace', message, {...context, internal: true}),
|
|
472
|
+
isLevelEnabled: (level) => isNamespaceEnabled(namespace) && isLevelEnabled(level),
|
|
473
|
+
child: (childContext) => createLogger(namespace, {...baseContext, ...childContext}),
|
|
474
|
+
getInstanceContext: () => baseContext?.instanceContext,
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return logger
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Create a performance timer for measuring operation duration
|
|
482
|
+
* @internal
|
|
483
|
+
*/
|
|
484
|
+
export function createTimer(
|
|
485
|
+
namespace: string,
|
|
486
|
+
operation: string,
|
|
487
|
+
): {end: (message?: string, context?: LogContext) => number} {
|
|
488
|
+
const logger = createLogger(namespace)
|
|
489
|
+
const start = performance.now()
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
end: (message?: string, context?: LogContext): number => {
|
|
493
|
+
const duration = performance.now() - start
|
|
494
|
+
logger.debug(message ?? `${operation} completed`, {
|
|
495
|
+
...context,
|
|
496
|
+
operation,
|
|
497
|
+
duration,
|
|
498
|
+
})
|
|
499
|
+
return duration
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Utility to log RxJS operator execution (for maintainers)
|
|
506
|
+
* Will be exported in future PR when logging is added to stores
|
|
507
|
+
* @internal
|
|
508
|
+
*/
|
|
509
|
+
/* c8 ignore next 4 */
|
|
510
|
+
function logRxJSOperator(namespace: string, operator: string, context?: LogContext): void {
|
|
511
|
+
const logger = createLogger(namespace)
|
|
512
|
+
logger.trace(`RxJS: ${operator}`, {...context, internal: true, operator})
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Extract instance context from a SanityInstance for logging
|
|
517
|
+
* Will be exported in future PR when logging is added to stores
|
|
518
|
+
* @param instance - The SanityInstance to extract context from
|
|
519
|
+
* @returns Instance context suitable for logging
|
|
520
|
+
* @internal
|
|
521
|
+
*/
|
|
522
|
+
/* c8 ignore next 7 */
|
|
523
|
+
function getInstanceContext(instance: {
|
|
524
|
+
instanceId?: string
|
|
525
|
+
config?: {projectId?: string; dataset?: string}
|
|
526
|
+
}): InstanceContext {
|
|
527
|
+
return {
|
|
528
|
+
instanceId: instance.instanceId,
|
|
529
|
+
projectId: instance.config?.projectId,
|
|
530
|
+
dataset: instance.config?.dataset,
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Prevent unused function warnings - these will be exported and used in future PRs
|
|
535
|
+
/* c8 ignore next 2 */
|
|
536
|
+
void logRxJSOperator
|
|
537
|
+
void getInstanceContext
|