@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,757 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {configureLogging as publicConfigureLogging} from '../config/loggingConfig'
|
|
4
|
+
import {
|
|
5
|
+
configureLogging,
|
|
6
|
+
createLogger,
|
|
7
|
+
createTimer,
|
|
8
|
+
getLoggingConfig,
|
|
9
|
+
type LogHandler,
|
|
10
|
+
parseDebugEnvVar,
|
|
11
|
+
resetLogging,
|
|
12
|
+
} from './logger'
|
|
13
|
+
|
|
14
|
+
describe('logger', () => {
|
|
15
|
+
const mockHandler: LogHandler = {
|
|
16
|
+
error: vi.fn(),
|
|
17
|
+
warn: vi.fn(),
|
|
18
|
+
info: vi.fn(),
|
|
19
|
+
debug: vi.fn(),
|
|
20
|
+
trace: vi.fn(),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
resetLogging()
|
|
25
|
+
vi.clearAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
resetLogging()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('configureLogging', () => {
|
|
33
|
+
it('should configure log level', () => {
|
|
34
|
+
configureLogging({level: 'debug', namespaces: ['*']})
|
|
35
|
+
const config = getLoggingConfig()
|
|
36
|
+
expect(config.level).toBe('debug')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should configure namespaces', () => {
|
|
40
|
+
configureLogging({namespaces: ['auth', 'document']})
|
|
41
|
+
const config = getLoggingConfig()
|
|
42
|
+
expect(config.namespaces).toEqual(['auth', 'document'])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should configure internal logging', () => {
|
|
46
|
+
configureLogging({internal: true})
|
|
47
|
+
const config = getLoggingConfig()
|
|
48
|
+
expect(config.internal).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should configure custom handler', () => {
|
|
52
|
+
configureLogging({handler: mockHandler})
|
|
53
|
+
const config = getLoggingConfig()
|
|
54
|
+
expect(config.handler).toBe(mockHandler)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should log when logging is configured', () => {
|
|
58
|
+
publicConfigureLogging({
|
|
59
|
+
level: 'info',
|
|
60
|
+
namespaces: ['sdk'],
|
|
61
|
+
handler: mockHandler,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
65
|
+
expect.stringContaining('[sdk] Logging configured'),
|
|
66
|
+
expect.objectContaining({
|
|
67
|
+
level: 'info',
|
|
68
|
+
namespaces: ['sdk'],
|
|
69
|
+
internal: false,
|
|
70
|
+
source: 'programmatic',
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should log to console.info when no custom handler is provided', () => {
|
|
76
|
+
const consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
|
|
77
|
+
|
|
78
|
+
publicConfigureLogging({
|
|
79
|
+
level: 'info',
|
|
80
|
+
namespaces: ['sdk'],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
84
|
+
expect.stringContaining('[sdk] Logging configured'),
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
level: 'info',
|
|
87
|
+
namespaces: ['sdk'],
|
|
88
|
+
internal: false,
|
|
89
|
+
source: 'programmatic',
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
consoleInfoSpy.mockRestore()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('createLogger', () => {
|
|
98
|
+
it('should create a logger for a namespace', () => {
|
|
99
|
+
const logger = createLogger('auth')
|
|
100
|
+
expect(logger.namespace).toBe('auth')
|
|
101
|
+
expect(typeof logger.error).toBe('function')
|
|
102
|
+
expect(typeof logger.warn).toBe('function')
|
|
103
|
+
expect(typeof logger.info).toBe('function')
|
|
104
|
+
expect(typeof logger.debug).toBe('function')
|
|
105
|
+
expect(typeof logger.trace).toBe('function')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should log at enabled levels', () => {
|
|
109
|
+
configureLogging({
|
|
110
|
+
level: 'info',
|
|
111
|
+
namespaces: ['auth'],
|
|
112
|
+
handler: mockHandler,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const logger = createLogger('auth')
|
|
116
|
+
logger.info('test message', {foo: 'bar'})
|
|
117
|
+
|
|
118
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
119
|
+
expect.stringContaining('[INFO]'),
|
|
120
|
+
expect.objectContaining({foo: 'bar'}),
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should not log at disabled levels', () => {
|
|
125
|
+
configureLogging({
|
|
126
|
+
level: 'warn',
|
|
127
|
+
namespaces: ['auth'],
|
|
128
|
+
handler: mockHandler,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const logger = createLogger('auth')
|
|
132
|
+
logger.info('test message')
|
|
133
|
+
logger.debug('test message')
|
|
134
|
+
|
|
135
|
+
expect(mockHandler.info).not.toHaveBeenCalled()
|
|
136
|
+
expect(mockHandler.debug).not.toHaveBeenCalled()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should not log for disabled namespaces', () => {
|
|
140
|
+
configureLogging({
|
|
141
|
+
level: 'info',
|
|
142
|
+
namespaces: ['auth'],
|
|
143
|
+
handler: mockHandler,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const logger = createLogger('document')
|
|
147
|
+
logger.info('test message')
|
|
148
|
+
|
|
149
|
+
expect(mockHandler.info).not.toHaveBeenCalled()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should log for wildcard namespace', () => {
|
|
153
|
+
configureLogging({
|
|
154
|
+
level: 'info',
|
|
155
|
+
namespaces: ['*'],
|
|
156
|
+
handler: mockHandler,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const authLogger = createLogger('auth')
|
|
160
|
+
const docLogger = createLogger('document')
|
|
161
|
+
|
|
162
|
+
authLogger.info('auth message')
|
|
163
|
+
docLogger.info('doc message')
|
|
164
|
+
|
|
165
|
+
expect(mockHandler.info).toHaveBeenCalledTimes(2)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should sanitize sensitive data', () => {
|
|
169
|
+
configureLogging({
|
|
170
|
+
level: 'info',
|
|
171
|
+
namespaces: ['auth'],
|
|
172
|
+
handler: mockHandler,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const logger = createLogger('auth')
|
|
176
|
+
logger.info('test', {token: 'secret123', password: 'pass123'})
|
|
177
|
+
|
|
178
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
179
|
+
expect.any(String),
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
token: '[REDACTED]',
|
|
182
|
+
password: '[REDACTED]',
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should support child loggers with base context', () => {
|
|
188
|
+
configureLogging({
|
|
189
|
+
level: 'info',
|
|
190
|
+
namespaces: ['auth'],
|
|
191
|
+
handler: mockHandler,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const logger = createLogger('auth')
|
|
195
|
+
const childLogger = logger.child({userId: '123'})
|
|
196
|
+
childLogger.info('test message', {action: 'login'})
|
|
197
|
+
|
|
198
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
199
|
+
expect.any(String),
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
userId: '123',
|
|
202
|
+
action: 'login',
|
|
203
|
+
}),
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should check if level is enabled', () => {
|
|
208
|
+
configureLogging({
|
|
209
|
+
level: 'info',
|
|
210
|
+
namespaces: ['auth'],
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const logger = createLogger('auth')
|
|
214
|
+
expect(logger.isLevelEnabled('info')).toBe(true)
|
|
215
|
+
expect(logger.isLevelEnabled('debug')).toBe(false)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('log levels', () => {
|
|
220
|
+
it('should respect level hierarchy', () => {
|
|
221
|
+
configureLogging({
|
|
222
|
+
level: 'warn',
|
|
223
|
+
namespaces: ['*'],
|
|
224
|
+
handler: mockHandler,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const logger = createLogger('core')
|
|
228
|
+
|
|
229
|
+
logger.error('error')
|
|
230
|
+
logger.warn('warn')
|
|
231
|
+
logger.info('info')
|
|
232
|
+
logger.debug('debug')
|
|
233
|
+
logger.trace('trace')
|
|
234
|
+
|
|
235
|
+
expect(mockHandler.error).toHaveBeenCalled()
|
|
236
|
+
expect(mockHandler.warn).toHaveBeenCalled()
|
|
237
|
+
expect(mockHandler.info).not.toHaveBeenCalled()
|
|
238
|
+
expect(mockHandler.debug).not.toHaveBeenCalled()
|
|
239
|
+
expect(mockHandler.trace).not.toHaveBeenCalled()
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('internal logging', () => {
|
|
244
|
+
it('should not log internal messages by default', () => {
|
|
245
|
+
configureLogging({
|
|
246
|
+
level: 'trace',
|
|
247
|
+
namespaces: ['*'],
|
|
248
|
+
internal: false,
|
|
249
|
+
handler: mockHandler,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const logger = createLogger('store')
|
|
253
|
+
logger.trace('internal message')
|
|
254
|
+
|
|
255
|
+
expect(mockHandler.trace).not.toHaveBeenCalled()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should log internal messages when enabled', () => {
|
|
259
|
+
configureLogging({
|
|
260
|
+
level: 'trace',
|
|
261
|
+
namespaces: ['*'],
|
|
262
|
+
internal: true,
|
|
263
|
+
handler: mockHandler,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const logger = createLogger('store')
|
|
267
|
+
logger.trace('internal message')
|
|
268
|
+
|
|
269
|
+
expect(mockHandler.trace).toHaveBeenCalled()
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('createTimer', () => {
|
|
274
|
+
it('should measure operation duration', async () => {
|
|
275
|
+
configureLogging({
|
|
276
|
+
level: 'debug',
|
|
277
|
+
namespaces: ['query'],
|
|
278
|
+
handler: mockHandler,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const timer = createTimer('query', 'fetchQuery')
|
|
282
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
283
|
+
const duration = timer.end()
|
|
284
|
+
|
|
285
|
+
expect(duration).toBeGreaterThanOrEqual(8) // Allow for timing variance
|
|
286
|
+
expect(mockHandler.debug).toHaveBeenCalledWith(
|
|
287
|
+
expect.stringContaining('fetchQuery completed'),
|
|
288
|
+
expect.objectContaining({
|
|
289
|
+
operation: 'fetchQuery',
|
|
290
|
+
duration: expect.any(Number),
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('should accept custom message and context', () => {
|
|
296
|
+
configureLogging({
|
|
297
|
+
level: 'debug',
|
|
298
|
+
namespaces: ['query'],
|
|
299
|
+
handler: mockHandler,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
const timer = createTimer('query', 'fetchQuery')
|
|
303
|
+
timer.end('Query fetch succeeded', {queryId: '123'})
|
|
304
|
+
|
|
305
|
+
expect(mockHandler.debug).toHaveBeenCalledWith(
|
|
306
|
+
expect.stringContaining('Query fetch succeeded'),
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
operation: 'fetchQuery',
|
|
309
|
+
queryId: '123',
|
|
310
|
+
}),
|
|
311
|
+
)
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe('message formatting', () => {
|
|
316
|
+
it('should include timestamp by default', () => {
|
|
317
|
+
configureLogging({
|
|
318
|
+
level: 'info',
|
|
319
|
+
namespaces: ['auth'],
|
|
320
|
+
timestamps: true,
|
|
321
|
+
handler: mockHandler,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const logger = createLogger('auth')
|
|
325
|
+
logger.info('test')
|
|
326
|
+
|
|
327
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
328
|
+
expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/),
|
|
329
|
+
undefined,
|
|
330
|
+
)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('should exclude timestamp when disabled', () => {
|
|
334
|
+
configureLogging({
|
|
335
|
+
level: 'info',
|
|
336
|
+
namespaces: ['auth'],
|
|
337
|
+
timestamps: false,
|
|
338
|
+
handler: mockHandler,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const logger = createLogger('auth')
|
|
342
|
+
logger.info('test')
|
|
343
|
+
|
|
344
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
345
|
+
expect.not.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/),
|
|
346
|
+
undefined,
|
|
347
|
+
)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should include log level and namespace', () => {
|
|
351
|
+
configureLogging({
|
|
352
|
+
level: 'info',
|
|
353
|
+
namespaces: ['auth'],
|
|
354
|
+
handler: mockHandler,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const logger = createLogger('auth')
|
|
358
|
+
logger.info('test message')
|
|
359
|
+
|
|
360
|
+
expect(mockHandler.info).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), undefined)
|
|
361
|
+
expect(mockHandler.info).toHaveBeenCalledWith(expect.stringContaining('[auth]'), undefined)
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe('resetLogging', () => {
|
|
366
|
+
it('should reset to default configuration', () => {
|
|
367
|
+
configureLogging({
|
|
368
|
+
level: 'trace',
|
|
369
|
+
namespaces: ['*'],
|
|
370
|
+
internal: true,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
resetLogging()
|
|
374
|
+
|
|
375
|
+
const config = getLoggingConfig()
|
|
376
|
+
expect(config.level).toBe('warn')
|
|
377
|
+
expect(config.namespaces).toEqual([])
|
|
378
|
+
expect(config.internal).toBe(false)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('production environment detection', () => {
|
|
383
|
+
const originalEnv = process.env['NODE_ENV']
|
|
384
|
+
|
|
385
|
+
afterEach(() => {
|
|
386
|
+
// Restore original NODE_ENV
|
|
387
|
+
process.env['NODE_ENV'] = originalEnv
|
|
388
|
+
resetLogging()
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('should disable logging in production by default', () => {
|
|
392
|
+
process.env['NODE_ENV'] = 'production'
|
|
393
|
+
|
|
394
|
+
configureLogging({
|
|
395
|
+
level: 'info',
|
|
396
|
+
namespaces: ['*'],
|
|
397
|
+
handler: mockHandler,
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const logger = createLogger('auth')
|
|
401
|
+
logger.info('test message')
|
|
402
|
+
|
|
403
|
+
// Should not log in production by default
|
|
404
|
+
expect(mockHandler.info).not.toHaveBeenCalled()
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('should enable logging in production when enableInProduction is true', () => {
|
|
408
|
+
process.env['NODE_ENV'] = 'production'
|
|
409
|
+
|
|
410
|
+
configureLogging({
|
|
411
|
+
level: 'info',
|
|
412
|
+
namespaces: ['*'],
|
|
413
|
+
enableInProduction: true,
|
|
414
|
+
handler: mockHandler,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const logger = createLogger('auth')
|
|
418
|
+
logger.info('test message')
|
|
419
|
+
|
|
420
|
+
// Should log when explicitly enabled
|
|
421
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
422
|
+
expect.stringContaining('[INFO] [auth] test message'),
|
|
423
|
+
undefined,
|
|
424
|
+
)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('should enable logging in non-production environments', () => {
|
|
428
|
+
process.env['NODE_ENV'] = 'development'
|
|
429
|
+
|
|
430
|
+
configureLogging({
|
|
431
|
+
level: 'info',
|
|
432
|
+
namespaces: ['*'],
|
|
433
|
+
handler: mockHandler,
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
const logger = createLogger('auth')
|
|
437
|
+
logger.info('test message')
|
|
438
|
+
|
|
439
|
+
// Should log in development
|
|
440
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
441
|
+
expect.stringContaining('[INFO] [auth] test message'),
|
|
442
|
+
undefined,
|
|
443
|
+
)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('should enable logging when NODE_ENV is not set', () => {
|
|
447
|
+
delete process.env['NODE_ENV']
|
|
448
|
+
|
|
449
|
+
configureLogging({
|
|
450
|
+
level: 'info',
|
|
451
|
+
namespaces: ['*'],
|
|
452
|
+
handler: mockHandler,
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
const logger = createLogger('auth')
|
|
456
|
+
logger.info('test message')
|
|
457
|
+
|
|
458
|
+
// Should log when NODE_ENV is undefined
|
|
459
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
460
|
+
expect.stringContaining('[INFO] [auth] test message'),
|
|
461
|
+
undefined,
|
|
462
|
+
)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('should respect enableInProduction: false in production', () => {
|
|
466
|
+
process.env['NODE_ENV'] = 'production'
|
|
467
|
+
|
|
468
|
+
configureLogging({
|
|
469
|
+
level: 'info',
|
|
470
|
+
namespaces: ['*'],
|
|
471
|
+
enableInProduction: false,
|
|
472
|
+
handler: mockHandler,
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
const logger = createLogger('auth')
|
|
476
|
+
logger.info('test message')
|
|
477
|
+
|
|
478
|
+
// Should not log when explicitly disabled
|
|
479
|
+
expect(mockHandler.info).not.toHaveBeenCalled()
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
describe('parseDebugEnvVar', () => {
|
|
484
|
+
const originalDebug = process.env['DEBUG']
|
|
485
|
+
|
|
486
|
+
afterEach(() => {
|
|
487
|
+
// Restore original DEBUG
|
|
488
|
+
if (originalDebug === undefined) {
|
|
489
|
+
delete process.env['DEBUG']
|
|
490
|
+
} else {
|
|
491
|
+
process.env['DEBUG'] = originalDebug
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('should return null when DEBUG is not set', () => {
|
|
496
|
+
delete process.env['DEBUG']
|
|
497
|
+
expect(parseDebugEnvVar()).toBeNull()
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('should return null when DEBUG does not include "sanity"', () => {
|
|
501
|
+
process.env['DEBUG'] = 'express:*'
|
|
502
|
+
expect(parseDebugEnvVar()).toBeNull()
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('should parse DEBUG=sanity:* to enable all namespaces at debug level', () => {
|
|
506
|
+
process.env['DEBUG'] = 'sanity:*'
|
|
507
|
+
const config = parseDebugEnvVar()
|
|
508
|
+
|
|
509
|
+
expect(config).toEqual({
|
|
510
|
+
level: 'debug',
|
|
511
|
+
namespaces: ['*'],
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('should parse DEBUG=sanity to enable all namespaces', () => {
|
|
516
|
+
process.env['DEBUG'] = 'sanity'
|
|
517
|
+
const config = parseDebugEnvVar()
|
|
518
|
+
|
|
519
|
+
expect(config).toEqual({
|
|
520
|
+
level: 'debug',
|
|
521
|
+
namespaces: ['*'],
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('should parse DEBUG=sanity:auth,sanity:document for specific namespaces', () => {
|
|
526
|
+
process.env['DEBUG'] = 'sanity:auth,sanity:document'
|
|
527
|
+
const config = parseDebugEnvVar()
|
|
528
|
+
|
|
529
|
+
expect(config).toEqual({
|
|
530
|
+
level: 'debug',
|
|
531
|
+
namespaces: ['auth', 'document'],
|
|
532
|
+
})
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
it('should parse DEBUG=sanity:trace:* to enable trace level', () => {
|
|
536
|
+
process.env['DEBUG'] = 'sanity:trace:*'
|
|
537
|
+
const config = parseDebugEnvVar()
|
|
538
|
+
|
|
539
|
+
expect(config).toEqual({
|
|
540
|
+
level: 'trace',
|
|
541
|
+
namespaces: ['*'],
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('should parse DEBUG=sanity:info:auth for custom level with namespace', () => {
|
|
546
|
+
process.env['DEBUG'] = 'sanity:info:auth'
|
|
547
|
+
const config = parseDebugEnvVar()
|
|
548
|
+
|
|
549
|
+
expect(config?.level).toBe('info')
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
it('should parse DEBUG=sanity:*:internal to enable internal logs', () => {
|
|
553
|
+
process.env['DEBUG'] = 'sanity:*:internal'
|
|
554
|
+
const config = parseDebugEnvVar()
|
|
555
|
+
|
|
556
|
+
expect(config).toEqual({
|
|
557
|
+
level: 'debug',
|
|
558
|
+
namespaces: ['*'],
|
|
559
|
+
internal: true,
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('should handle mixed DEBUG patterns with non-sanity values', () => {
|
|
564
|
+
process.env['DEBUG'] = 'express:*,sanity:auth,other:*'
|
|
565
|
+
const config = parseDebugEnvVar()
|
|
566
|
+
|
|
567
|
+
expect(config).toEqual({
|
|
568
|
+
level: 'debug',
|
|
569
|
+
namespaces: ['auth'],
|
|
570
|
+
})
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('should filter out wildcards from specific namespace list', () => {
|
|
574
|
+
process.env['DEBUG'] = 'sanity:auth,sanity:*'
|
|
575
|
+
const config = parseDebugEnvVar()
|
|
576
|
+
|
|
577
|
+
// Should prefer wildcard mode
|
|
578
|
+
expect(config?.namespaces).toContain('*')
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
describe('DEBUG environment variable integration', () => {
|
|
583
|
+
const originalDebug = process.env['DEBUG']
|
|
584
|
+
|
|
585
|
+
afterEach(() => {
|
|
586
|
+
// Restore original DEBUG
|
|
587
|
+
if (originalDebug === undefined) {
|
|
588
|
+
delete process.env['DEBUG']
|
|
589
|
+
} else {
|
|
590
|
+
process.env['DEBUG'] = originalDebug
|
|
591
|
+
}
|
|
592
|
+
resetLogging()
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it('should apply DEBUG env var configuration to logging', () => {
|
|
596
|
+
process.env['DEBUG'] = 'sanity:*'
|
|
597
|
+
|
|
598
|
+
// Simulate what happens on module init
|
|
599
|
+
const envConfig = parseDebugEnvVar()
|
|
600
|
+
if (envConfig) {
|
|
601
|
+
configureLogging({
|
|
602
|
+
...envConfig,
|
|
603
|
+
handler: mockHandler,
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const logger = createLogger('auth')
|
|
608
|
+
logger.debug('test message')
|
|
609
|
+
|
|
610
|
+
expect(mockHandler.debug).toHaveBeenCalled()
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('should allow programmatic config to override env var', () => {
|
|
614
|
+
process.env['DEBUG'] = 'sanity:*'
|
|
615
|
+
|
|
616
|
+
// First apply env var config
|
|
617
|
+
const envConfig = parseDebugEnvVar()
|
|
618
|
+
if (envConfig) {
|
|
619
|
+
configureLogging(envConfig)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Then override with programmatic config
|
|
623
|
+
configureLogging({
|
|
624
|
+
level: 'error',
|
|
625
|
+
namespaces: ['auth'],
|
|
626
|
+
handler: mockHandler,
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
const config = getLoggingConfig()
|
|
630
|
+
expect(config.level).toBe('error')
|
|
631
|
+
expect(config.namespaces).toEqual(['auth'])
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
describe('instance context', () => {
|
|
636
|
+
it('should include instance context in log output', () => {
|
|
637
|
+
configureLogging({
|
|
638
|
+
level: 'info',
|
|
639
|
+
namespaces: ['document'],
|
|
640
|
+
handler: mockHandler,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
const logger = createLogger('document', {
|
|
644
|
+
instanceContext: {
|
|
645
|
+
instanceId: 'abc123def456',
|
|
646
|
+
projectId: 'my-project',
|
|
647
|
+
dataset: 'production',
|
|
648
|
+
},
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
logger.info('Document updated')
|
|
652
|
+
|
|
653
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
654
|
+
expect.stringContaining('[project:my-project]'),
|
|
655
|
+
expect.any(Object),
|
|
656
|
+
)
|
|
657
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
658
|
+
expect.stringContaining('[dataset:production]'),
|
|
659
|
+
expect.any(Object),
|
|
660
|
+
)
|
|
661
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
662
|
+
expect.stringContaining('[instance:abc123de]'), // Truncated to 8 chars
|
|
663
|
+
expect.any(Object),
|
|
664
|
+
)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it('should work without instance context', () => {
|
|
668
|
+
configureLogging({
|
|
669
|
+
level: 'info',
|
|
670
|
+
namespaces: ['auth'],
|
|
671
|
+
handler: mockHandler,
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
const logger = createLogger('auth')
|
|
675
|
+
logger.info('Static operation')
|
|
676
|
+
|
|
677
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
678
|
+
expect.stringContaining('[INFO] [auth] Static operation'),
|
|
679
|
+
undefined,
|
|
680
|
+
)
|
|
681
|
+
expect(mockHandler.info).not.toHaveBeenCalledWith(
|
|
682
|
+
expect.stringContaining('[project:'),
|
|
683
|
+
expect.any(Object),
|
|
684
|
+
)
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
it('should include partial instance context', () => {
|
|
688
|
+
configureLogging({
|
|
689
|
+
level: 'info',
|
|
690
|
+
namespaces: ['query'],
|
|
691
|
+
handler: mockHandler,
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
const logger = createLogger('query', {
|
|
695
|
+
instanceContext: {
|
|
696
|
+
projectId: 'my-project',
|
|
697
|
+
// No dataset or instanceId
|
|
698
|
+
},
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
logger.info('Query executed')
|
|
702
|
+
|
|
703
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
704
|
+
expect.stringContaining('[project:my-project]'),
|
|
705
|
+
expect.any(Object),
|
|
706
|
+
)
|
|
707
|
+
expect(mockHandler.info).not.toHaveBeenCalledWith(
|
|
708
|
+
expect.stringContaining('[dataset:'),
|
|
709
|
+
expect.any(Object),
|
|
710
|
+
)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('should return instance context', () => {
|
|
714
|
+
const instanceContext = {
|
|
715
|
+
instanceId: 'test-instance',
|
|
716
|
+
projectId: 'test-project',
|
|
717
|
+
dataset: 'test-dataset',
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const logger = createLogger('auth', {instanceContext})
|
|
721
|
+
|
|
722
|
+
expect(logger.getInstanceContext()).toEqual(instanceContext)
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
it('should return undefined when no instance context', () => {
|
|
726
|
+
const logger = createLogger('auth')
|
|
727
|
+
|
|
728
|
+
expect(logger.getInstanceContext()).toBeUndefined()
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
it('should merge instance context with other context', () => {
|
|
732
|
+
configureLogging({
|
|
733
|
+
level: 'info',
|
|
734
|
+
namespaces: ['document'],
|
|
735
|
+
handler: mockHandler,
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
const logger = createLogger('document', {
|
|
739
|
+
instanceContext: {
|
|
740
|
+
projectId: 'my-project',
|
|
741
|
+
dataset: 'production',
|
|
742
|
+
},
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
logger.info('Document synced', {documentId: 'doc-123', syncTime: 150})
|
|
746
|
+
|
|
747
|
+
expect(mockHandler.info).toHaveBeenCalledWith(
|
|
748
|
+
expect.stringContaining('[project:my-project]'),
|
|
749
|
+
expect.objectContaining({
|
|
750
|
+
documentId: 'doc-123',
|
|
751
|
+
syncTime: 150,
|
|
752
|
+
instanceContext: expect.any(Object),
|
|
753
|
+
}),
|
|
754
|
+
)
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
})
|