@sanity/sdk 2.8.0 → 2.9.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/_chunks-dts/utils.d.ts +2396 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1460 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +87 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_chunks-es/version.js +7 -0
- package/dist/_chunks-es/version.js.map +1 -0
- package/dist/_exports/_internal.d.ts +64 -0
- package/dist/_exports/_internal.js +20 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +2 -2343
- package/dist/index.js +383 -1777
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +10 -1
- package/src/auth/authStore.test.ts +150 -1
- package/src/auth/authStore.ts +11 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +118 -1
- package/src/auth/refreshStampedToken.ts +3 -2
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +34 -7
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +14 -0
- package/src/client/clientStore.ts +2 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +6 -0
- package/src/document/actions.ts +18 -11
- package/src/document/applyDocumentActions.test.ts +7 -6
- package/src/document/applyDocumentActions.ts +10 -4
- package/src/document/documentStore.test.ts +536 -188
- package/src/document/documentStore.ts +142 -76
- package/src/document/events.ts +7 -2
- package/src/document/permissions.test.ts +18 -16
- package/src/document/permissions.ts +35 -11
- package/src/document/processActions.test.ts +359 -32
- package/src/document/processActions.ts +104 -76
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +43 -36
- package/src/document/sharedListener.ts +16 -6
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +9 -2
- package/src/presence/bifurTransport.ts +6 -1
- package/src/preview/getPreviewState.test.ts +115 -98
- package/src/preview/getPreviewState.ts +38 -60
- package/src/preview/previewProjectionUtils.test.ts +179 -0
- package/src/preview/previewProjectionUtils.ts +93 -0
- package/src/preview/resolvePreview.test.ts +42 -25
- package/src/preview/resolvePreview.ts +29 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +2 -1
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.ts +2 -1
- package/src/releases/getPerspectiveState.ts +7 -6
- package/src/releases/releasesStore.test.ts +20 -5
- package/src/releases/releasesStore.ts +20 -8
- package/src/store/createStateSourceAction.test.ts +62 -0
- package/src/store/createStateSourceAction.ts +34 -39
- package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
- package/src/telemetry/devMode.test.ts +52 -0
- package/src/telemetry/devMode.ts +40 -0
- package/src/telemetry/initTelemetry.test.ts +225 -0
- package/src/telemetry/initTelemetry.ts +205 -0
- package/src/telemetry/telemetryManager.test.ts +263 -0
- package/src/telemetry/telemetryManager.ts +187 -0
- package/src/users/usersStore.test.ts +1 -0
- package/src/users/usersStore.ts +5 -1
- package/src/utils/createFetcherStore.test.ts +6 -4
- package/src/utils/createFetcherStore.ts +2 -1
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/setCleanupTimeout.ts +24 -0
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- package/src/preview/util.ts +0 -13
|
@@ -30,6 +30,21 @@ describe('releasesStore', () => {
|
|
|
30
30
|
instance.dispose()
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
+
it('supports calls without options', () => {
|
|
34
|
+
const state = getActiveReleasesState(instance)
|
|
35
|
+
|
|
36
|
+
expect(state.getCurrent()).toBeUndefined()
|
|
37
|
+
expect(getQueryState).toHaveBeenCalledWith(
|
|
38
|
+
instance,
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
query: 'releases::all()',
|
|
41
|
+
perspective: 'raw',
|
|
42
|
+
source: undefined,
|
|
43
|
+
tag: 'releases',
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
33
48
|
it('should set active releases state when the releases query emits', async () => {
|
|
34
49
|
const teardown = vi.fn()
|
|
35
50
|
const subscriber = vi
|
|
@@ -58,7 +73,7 @@ describe('releasesStore', () => {
|
|
|
58
73
|
} as ReleaseDocument,
|
|
59
74
|
]
|
|
60
75
|
|
|
61
|
-
const state = getActiveReleasesState(instance)
|
|
76
|
+
const state = getActiveReleasesState(instance, {source: {projectId: 'test', dataset: 'test'}})
|
|
62
77
|
|
|
63
78
|
const [observer] = subscriber.mock.lastCall!
|
|
64
79
|
|
|
@@ -77,7 +92,7 @@ describe('releasesStore', () => {
|
|
|
77
92
|
observable: releasesSubject.asObservable(),
|
|
78
93
|
} as StateSource<ReleaseDocument[] | undefined>)
|
|
79
94
|
|
|
80
|
-
const state = getActiveReleasesState(instance)
|
|
95
|
+
const state = getActiveReleasesState(instance, {source: {projectId: 'test', dataset: 'test'}})
|
|
81
96
|
|
|
82
97
|
// Initial state should be default
|
|
83
98
|
expect(state.getCurrent()).toBeUndefined() // Default initial state
|
|
@@ -124,7 +139,7 @@ describe('releasesStore', () => {
|
|
|
124
139
|
observable: of([]),
|
|
125
140
|
} as StateSource<ReleaseDocument[] | undefined>)
|
|
126
141
|
|
|
127
|
-
const state = getActiveReleasesState(instance)
|
|
142
|
+
const state = getActiveReleasesState(instance, {source: {projectId: 'test', dataset: 'test'}})
|
|
128
143
|
|
|
129
144
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
130
145
|
|
|
@@ -138,7 +153,7 @@ describe('releasesStore', () => {
|
|
|
138
153
|
getCurrent: () => null as unknown as ReleaseDocument[] | undefined,
|
|
139
154
|
observable: of(null as unknown as ReleaseDocument[] | undefined),
|
|
140
155
|
} as StateSource<ReleaseDocument[] | undefined>)
|
|
141
|
-
const state = getActiveReleasesState(instance)
|
|
156
|
+
const state = getActiveReleasesState(instance, {source: {projectId: 'test', dataset: 'test'}})
|
|
142
157
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
143
158
|
expect(state.getCurrent()).toEqual([])
|
|
144
159
|
|
|
@@ -160,7 +175,7 @@ describe('releasesStore', () => {
|
|
|
160
175
|
observable: subject.asObservable(),
|
|
161
176
|
} as StateSource<ReleaseDocument[] | undefined>)
|
|
162
177
|
|
|
163
|
-
const state = getActiveReleasesState(instance)
|
|
178
|
+
const state = getActiveReleasesState(instance, {source: {projectId: 'test', dataset: 'test'}})
|
|
164
179
|
|
|
165
180
|
subject.error(new Error('Query failed'))
|
|
166
181
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {type SanityDocument} from '@sanity/types'
|
|
2
2
|
import {map} from 'rxjs'
|
|
3
3
|
|
|
4
|
+
import {type DocumentSource, isDatasetSource} from '../config/sanityConfig'
|
|
4
5
|
/*
|
|
5
6
|
* Although this is an import dependency cycle, it is not a logical cycle:
|
|
6
7
|
* 1. releasesStore uses queryStore as a data source
|
|
@@ -10,8 +11,9 @@ import {map} from 'rxjs'
|
|
|
10
11
|
*/
|
|
11
12
|
// eslint-disable-next-line import/no-cycle
|
|
12
13
|
import {getQueryState} from '../query/queryStore'
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import {bindActionBySource, type BoundSourceKey} from '../store/createActionBinder'
|
|
15
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
16
|
+
import {createStateSourceAction, type StateSource} from '../store/createStateSourceAction'
|
|
15
17
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
16
18
|
import {sortReleases} from './utils/sortReleases'
|
|
17
19
|
|
|
@@ -39,7 +41,7 @@ export interface ReleasesStoreState {
|
|
|
39
41
|
error?: unknown
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
export const releasesStore = defineStore<ReleasesStoreState,
|
|
44
|
+
export const releasesStore = defineStore<ReleasesStoreState, BoundSourceKey>({
|
|
43
45
|
name: 'Releases',
|
|
44
46
|
getInitialState: (): ReleasesStoreState => ({
|
|
45
47
|
activeReleases: undefined,
|
|
@@ -54,25 +56,35 @@ export const releasesStore = defineStore<ReleasesStoreState, BoundDatasetKey>({
|
|
|
54
56
|
* Get the active releases from the store.
|
|
55
57
|
* @internal
|
|
56
58
|
*/
|
|
57
|
-
|
|
59
|
+
const _getActiveReleasesState = bindActionBySource(
|
|
58
60
|
releasesStore,
|
|
59
61
|
createStateSourceAction({
|
|
60
62
|
selector: ({state}, _?) => state.activeReleases,
|
|
61
63
|
}),
|
|
62
64
|
)
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Get the active releases from the store.
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
export const getActiveReleasesState = (
|
|
71
|
+
instance: SanityInstance,
|
|
72
|
+
options?: {source?: DocumentSource},
|
|
73
|
+
): StateSource<ReleaseDocument[] | undefined> =>
|
|
74
|
+
// bindActionBySource keyFn destructures { source } from the first param, so pass {} when no options
|
|
75
|
+
_getActiveReleasesState(instance, options ?? {})
|
|
76
|
+
|
|
64
77
|
const RELEASES_QUERY = 'releases::all()'
|
|
65
78
|
|
|
66
79
|
const subscribeToReleases = ({
|
|
67
80
|
instance,
|
|
68
81
|
state,
|
|
69
|
-
key: {
|
|
70
|
-
}: StoreContext<ReleasesStoreState,
|
|
82
|
+
key: {source},
|
|
83
|
+
}: StoreContext<ReleasesStoreState, BoundSourceKey>) => {
|
|
71
84
|
const {observable: releases$} = getQueryState<ReleaseDocument[]>(instance, {
|
|
72
85
|
query: RELEASES_QUERY,
|
|
73
86
|
perspective: 'raw',
|
|
74
|
-
|
|
75
|
-
dataset,
|
|
87
|
+
source: source && !isDatasetSource(source) ? source : undefined,
|
|
76
88
|
tag: 'releases',
|
|
77
89
|
})
|
|
78
90
|
return releases$
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {filter, firstValueFrom, timeout} from 'rxjs'
|
|
1
2
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
3
|
|
|
3
4
|
import {createSanityInstance, type SanityInstance} from './createSanityInstance'
|
|
@@ -194,4 +195,65 @@ describe('createStateSourceAction', () => {
|
|
|
194
195
|
expect(context1.instance).toBe(instance)
|
|
195
196
|
expect(context2.instance).toBe(secondInstance)
|
|
196
197
|
})
|
|
198
|
+
|
|
199
|
+
it('correctly observes values defined in the onSubscribe', async () => {
|
|
200
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
201
|
+
const source = createStateSourceAction({
|
|
202
|
+
selector: selector,
|
|
203
|
+
onSubscribe() {
|
|
204
|
+
state.set('update', {count: 1})
|
|
205
|
+
},
|
|
206
|
+
})({state, instance, key: null})
|
|
207
|
+
|
|
208
|
+
const value = await firstValueFrom(
|
|
209
|
+
source.observable.pipe(
|
|
210
|
+
filter((i) => i === 1),
|
|
211
|
+
timeout(10),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
expect(value).toBe(1)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('only invokes selector once on changes', () => {
|
|
218
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
219
|
+
const source = createStateSourceAction({selector: selector})({state, instance, key: null})
|
|
220
|
+
|
|
221
|
+
expect(selector).toBeCalledTimes(0)
|
|
222
|
+
|
|
223
|
+
// Now it should be called once:
|
|
224
|
+
const sub = source.observable.subscribe()
|
|
225
|
+
expect(selector).toBeCalledTimes(1)
|
|
226
|
+
|
|
227
|
+
// The observable should be shared so this shouldn't invoke it first.
|
|
228
|
+
const sub2 = source.observable.subscribe()
|
|
229
|
+
expect(selector).toBeCalledTimes(1)
|
|
230
|
+
|
|
231
|
+
// Updating the value should only invoke it once:
|
|
232
|
+
state.set('update', {count: 1})
|
|
233
|
+
expect(selector).toBeCalledTimes(2)
|
|
234
|
+
|
|
235
|
+
sub2.unsubscribe()
|
|
236
|
+
sub.unsubscribe()
|
|
237
|
+
|
|
238
|
+
// Once everyone has unsubscribed it should be invoked again.
|
|
239
|
+
const sub3 = source.observable.subscribe()
|
|
240
|
+
expect(selector).toBeCalledTimes(3)
|
|
241
|
+
sub3.unsubscribe()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('only subscribes once when mixing subscribe/observable', () => {
|
|
245
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
246
|
+
const source = createStateSourceAction({selector: selector})({state, instance, key: null})
|
|
247
|
+
|
|
248
|
+
expect(selector).toBeCalledTimes(0)
|
|
249
|
+
|
|
250
|
+
const sub = source.observable.subscribe()
|
|
251
|
+
expect(selector).toBeCalledTimes(1)
|
|
252
|
+
|
|
253
|
+
const sub2 = source.subscribe()
|
|
254
|
+
expect(selector).toBeCalledTimes(1)
|
|
255
|
+
|
|
256
|
+
sub.unsubscribe()
|
|
257
|
+
sub2()
|
|
258
|
+
})
|
|
197
259
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {distinctUntilChanged, map, Observable,
|
|
1
|
+
import {defer, distinctUntilChanged, finalize, map, Observable, shareReplay, skip} from 'rxjs'
|
|
2
2
|
|
|
3
3
|
import {type StoreAction} from './createActionBinder'
|
|
4
4
|
import {type SanityInstance} from './createSanityInstance'
|
|
@@ -187,8 +187,7 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
|
|
|
187
187
|
function stateSourceAction(context: StoreContext<TState, TKey>, ...params: TParams) {
|
|
188
188
|
const {state, instance} = context
|
|
189
189
|
|
|
190
|
-
const getCurrent = () => {
|
|
191
|
-
const currentState = state.get()
|
|
190
|
+
const getCurrent = (currentState: TState) => {
|
|
192
191
|
if (typeof currentState !== 'object' || currentState === null) {
|
|
193
192
|
throw new Error(
|
|
194
193
|
`Expected store state to be an object but got "${typeof currentState}" instead`,
|
|
@@ -208,53 +207,49 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
|
|
|
208
207
|
return selector(selectorContext, ...params)
|
|
209
208
|
}
|
|
210
209
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const cleanup = subscribeHandler?.(context, ...params)
|
|
210
|
+
// `state.observable` will emit the current value immediately and
|
|
211
|
+
// hence we inherit the same behavior here.
|
|
212
|
+
let values = state.observable.pipe(map(getCurrent), distinctUntilChanged(isEqual))
|
|
215
213
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
if (subscribeHandler) {
|
|
215
|
+
values = withSubscribeHook(values, () => subscribeHandler(context, ...params))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Share but replay the latest value so every subscriber gets an
|
|
219
|
+
// initial synchronous emission, matching `state.observable`. That keeps
|
|
220
|
+
// `skip(1)` in `subscribe()` aligned with "skip current snapshot" rather than
|
|
221
|
+
// silently eating the first real update after multicasting.
|
|
222
|
+
const sharedValues = values.pipe(shareReplay({bufferSize: 1, refCount: true}))
|
|
223
|
+
|
|
224
|
+
const subscribe = (onStoreChanged?: () => void) => {
|
|
225
|
+
const subscription = sharedValues.pipe(skip(1)).subscribe({
|
|
226
|
+
next: () => onStoreChanged?.(),
|
|
227
|
+
// Propagate selector errors to both subscription types
|
|
228
|
+
error: () => onStoreChanged?.(),
|
|
229
|
+
})
|
|
231
230
|
|
|
232
231
|
return () => {
|
|
233
232
|
subscription.unsubscribe()
|
|
234
|
-
cleanup?.()
|
|
235
233
|
}
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
// Create shared observable that handles multiple subscribers efficiently
|
|
239
|
-
const observable = new Observable<TReturn>((observer) => {
|
|
240
|
-
const emitCurrent = () => {
|
|
241
|
-
try {
|
|
242
|
-
observer.next(getCurrent())
|
|
243
|
-
} catch (error) {
|
|
244
|
-
observer.error(error)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Emit immediately on subscription
|
|
248
|
-
emitCurrent()
|
|
249
|
-
return subscribe(emitCurrent)
|
|
250
|
-
}).pipe(share())
|
|
251
|
-
|
|
252
236
|
return {
|
|
253
|
-
getCurrent,
|
|
237
|
+
getCurrent: () => getCurrent(state.get()),
|
|
254
238
|
subscribe,
|
|
255
|
-
observable,
|
|
239
|
+
observable: sharedValues,
|
|
256
240
|
}
|
|
257
241
|
}
|
|
258
242
|
|
|
259
243
|
return stateSourceAction
|
|
260
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates a new Observable which wraps an existing Observable which will invoke
|
|
248
|
+
* the function when a new subscriber appears.
|
|
249
|
+
*/
|
|
250
|
+
function withSubscribeHook<T>(obs: Observable<T>, fn: () => void | (() => void)): Observable<T> {
|
|
251
|
+
return defer(() => {
|
|
252
|
+
const cleanup = fn()
|
|
253
|
+
return cleanup ? obs.pipe(finalize(() => cleanup())) : obs
|
|
254
|
+
})
|
|
255
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {defineEvent} from '@sanity/telemetry'
|
|
2
|
+
|
|
3
|
+
/** @internal */
|
|
4
|
+
export const SDKDevSessionStarted = defineEvent<{
|
|
5
|
+
version: string
|
|
6
|
+
projectId: string
|
|
7
|
+
perspective: string
|
|
8
|
+
authMethod: string
|
|
9
|
+
}>({
|
|
10
|
+
name: 'SDK Dev Session Started',
|
|
11
|
+
version: 1,
|
|
12
|
+
description: 'SDK instance created in development mode',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** @internal */
|
|
16
|
+
export const SDKHookMounted = defineEvent<{
|
|
17
|
+
hookName: string
|
|
18
|
+
}>({
|
|
19
|
+
name: 'SDK Hook Mounted',
|
|
20
|
+
version: 1,
|
|
21
|
+
description: 'An SDK hook was mounted for the first time in this session',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** @internal */
|
|
25
|
+
export const SDKDevSessionEnded = defineEvent<{
|
|
26
|
+
durationSeconds: number
|
|
27
|
+
hooksUsed: string[]
|
|
28
|
+
}>({
|
|
29
|
+
name: 'SDK Dev Session Ended',
|
|
30
|
+
version: 1,
|
|
31
|
+
description: 'SDK instance disposed in development mode',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/** @internal */
|
|
35
|
+
export const SDKDevError = defineEvent<{
|
|
36
|
+
errorType: string
|
|
37
|
+
hookName: string
|
|
38
|
+
}>({
|
|
39
|
+
name: 'SDK Dev Error',
|
|
40
|
+
version: 1,
|
|
41
|
+
description: 'Runtime error caught during SDK development',
|
|
42
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {isDevMode} from './devMode'
|
|
4
|
+
|
|
5
|
+
describe('isDevMode', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
vi.unstubAllEnvs()
|
|
8
|
+
vi.unstubAllGlobals()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('returns false when NODE_ENV is production', () => {
|
|
12
|
+
vi.stubEnv('NODE_ENV', 'production')
|
|
13
|
+
vi.stubGlobal('window', undefined)
|
|
14
|
+
expect(isDevMode()).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns true when running on localhost', () => {
|
|
18
|
+
vi.stubEnv('NODE_ENV', 'development')
|
|
19
|
+
vi.stubGlobal('window', {
|
|
20
|
+
location: {href: 'http://localhost:3000/'},
|
|
21
|
+
})
|
|
22
|
+
expect(isDevMode()).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns true when running on 127.0.0.1', () => {
|
|
26
|
+
vi.stubEnv('NODE_ENV', 'development')
|
|
27
|
+
vi.stubGlobal('window', {
|
|
28
|
+
location: {href: 'http://127.0.0.1:3000/'},
|
|
29
|
+
})
|
|
30
|
+
expect(isDevMode()).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns false for a non-local URL', () => {
|
|
34
|
+
vi.stubEnv('NODE_ENV', 'test')
|
|
35
|
+
vi.stubGlobal('window', {
|
|
36
|
+
location: {href: 'https://myapp.sanity.studio/'},
|
|
37
|
+
})
|
|
38
|
+
expect(isDevMode()).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns true when NODE_ENV is development and no window', () => {
|
|
42
|
+
vi.stubEnv('NODE_ENV', 'development')
|
|
43
|
+
vi.stubGlobal('window', undefined)
|
|
44
|
+
expect(isDevMode()).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns false when NODE_ENV is test and no window', () => {
|
|
48
|
+
vi.stubEnv('NODE_ENV', 'test')
|
|
49
|
+
vi.stubGlobal('window', undefined)
|
|
50
|
+
expect(isDevMode()).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether the current URL points to a local development server.
|
|
3
|
+
*
|
|
4
|
+
* @param win - The window object to check
|
|
5
|
+
* @returns True if running on localhost or 127.0.0.1
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
function isLocalUrl(win: Window): boolean {
|
|
9
|
+
const url = win.location?.href
|
|
10
|
+
if (!url) return false
|
|
11
|
+
return (
|
|
12
|
+
url.startsWith('http://localhost') ||
|
|
13
|
+
url.startsWith('https://localhost') ||
|
|
14
|
+
url.startsWith('http://127.0.0.1') ||
|
|
15
|
+
url.startsWith('https://127.0.0.1')
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Determines whether the SDK should enable dev-mode telemetry.
|
|
21
|
+
*
|
|
22
|
+
* Combines a browser URL check (localhost/127.0.0.1) with a Node.js
|
|
23
|
+
* environment variable check (`NODE_ENV === 'development'`). Returns
|
|
24
|
+
* false in production environments so bundlers can tree-shake the
|
|
25
|
+
* telemetry code path entirely.
|
|
26
|
+
*
|
|
27
|
+
* @returns True if the SDK is running in a development environment
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export function isDevMode(): boolean {
|
|
31
|
+
if (typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'production') {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof window !== 'undefined') {
|
|
36
|
+
return isLocalUrl(window)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'development'
|
|
40
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {createSanityInstance} from '../store/createSanityInstance'
|
|
4
|
+
import {isDevMode} from './devMode'
|
|
5
|
+
import {getTelemetryManager, initTelemetry, trackHookMounted} from './initTelemetry'
|
|
6
|
+
import {createTelemetryManager} from './telemetryManager'
|
|
7
|
+
|
|
8
|
+
vi.mock('./devMode', () => ({
|
|
9
|
+
isDevMode: vi.fn(() => false),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('./telemetryManager', () => ({
|
|
13
|
+
createTelemetryManager: vi.fn(() => ({
|
|
14
|
+
checkConsent: vi.fn(() => Promise.resolve(true)),
|
|
15
|
+
logSessionStarted: vi.fn(),
|
|
16
|
+
logHookFirstUsed: vi.fn(),
|
|
17
|
+
logDevError: vi.fn(),
|
|
18
|
+
endSession: vi.fn(),
|
|
19
|
+
dispose: vi.fn(),
|
|
20
|
+
hooksUsed: new Set(),
|
|
21
|
+
})),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
vi.mock('../client/clientStore', () => ({
|
|
25
|
+
getClient: vi.fn(() => ({})),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
vi.mock('../auth/authStore', () => ({
|
|
29
|
+
getTokenState: vi.fn(() => ({
|
|
30
|
+
getCurrent: vi.fn(() => 'mock-token'),
|
|
31
|
+
observable: {subscribe: vi.fn()},
|
|
32
|
+
})),
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Flush the microtask queue so the dynamic imports in initTelemetry
|
|
37
|
+
* have time to resolve before assertions run.
|
|
38
|
+
*/
|
|
39
|
+
const flushPromises = () => new Promise<void>((r) => setTimeout(r, 0))
|
|
40
|
+
|
|
41
|
+
describe('initTelemetry', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.restoreAllMocks()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('does nothing when dev mode is disabled', async () => {
|
|
51
|
+
vi.mocked(isDevMode).mockReturnValue(false)
|
|
52
|
+
|
|
53
|
+
const instance = createSanityInstance()
|
|
54
|
+
|
|
55
|
+
initTelemetry(instance, 'abc123')
|
|
56
|
+
await flushPromises()
|
|
57
|
+
|
|
58
|
+
expect(createTelemetryManager).not.toHaveBeenCalled()
|
|
59
|
+
instance.dispose()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does nothing when no projectId is provided', async () => {
|
|
63
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
64
|
+
|
|
65
|
+
const instance = createSanityInstance()
|
|
66
|
+
initTelemetry(instance, '')
|
|
67
|
+
await flushPromises()
|
|
68
|
+
|
|
69
|
+
expect(createTelemetryManager).not.toHaveBeenCalled()
|
|
70
|
+
instance.dispose()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('initializes telemetry in dev mode with a projectId', async () => {
|
|
74
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
75
|
+
|
|
76
|
+
const instance = createSanityInstance()
|
|
77
|
+
|
|
78
|
+
initTelemetry(instance, 'abc123')
|
|
79
|
+
await flushPromises()
|
|
80
|
+
|
|
81
|
+
expect(createTelemetryManager).toHaveBeenCalledWith(
|
|
82
|
+
expect.objectContaining({
|
|
83
|
+
sessionId: instance.instanceId,
|
|
84
|
+
projectId: 'abc123',
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const manager = vi.mocked(createTelemetryManager).mock.results[0].value
|
|
89
|
+
expect(manager.logSessionStarted).toHaveBeenCalledWith(
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
projectId: 'abc123',
|
|
92
|
+
perspective: 'published',
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
instance.dispose()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('registers manager in the WeakMap', async () => {
|
|
100
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
101
|
+
|
|
102
|
+
const instance = createSanityInstance()
|
|
103
|
+
|
|
104
|
+
initTelemetry(instance, 'abc123')
|
|
105
|
+
await flushPromises()
|
|
106
|
+
|
|
107
|
+
expect(getTelemetryManager(instance)).toBeDefined()
|
|
108
|
+
|
|
109
|
+
instance.dispose()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('does not initialize if instance is already disposed', async () => {
|
|
113
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
114
|
+
|
|
115
|
+
const instance = createSanityInstance()
|
|
116
|
+
|
|
117
|
+
instance.dispose()
|
|
118
|
+
initTelemetry(instance, 'abc123')
|
|
119
|
+
await flushPromises()
|
|
120
|
+
|
|
121
|
+
const manager = vi.mocked(createTelemetryManager).mock.results[0]?.value
|
|
122
|
+
if (manager) {
|
|
123
|
+
expect(manager.logSessionStarted).not.toHaveBeenCalled()
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('calls endSession and removes manager on instance dispose', async () => {
|
|
128
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
129
|
+
|
|
130
|
+
const instance = createSanityInstance()
|
|
131
|
+
|
|
132
|
+
initTelemetry(instance, 'abc123')
|
|
133
|
+
await flushPromises()
|
|
134
|
+
|
|
135
|
+
const manager = vi.mocked(createTelemetryManager).mock.results[0].value
|
|
136
|
+
expect(getTelemetryManager(instance)).toBeDefined()
|
|
137
|
+
|
|
138
|
+
instance.dispose()
|
|
139
|
+
|
|
140
|
+
expect(manager.endSession).toHaveBeenCalled()
|
|
141
|
+
expect(getTelemetryManager(instance)).toBeUndefined()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('skips telemetry entirely when user has not opted in', async () => {
|
|
145
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
146
|
+
|
|
147
|
+
const instance = createSanityInstance()
|
|
148
|
+
|
|
149
|
+
vi.mocked(createTelemetryManager).mockReturnValueOnce({
|
|
150
|
+
checkConsent: vi.fn(() => Promise.resolve(false)),
|
|
151
|
+
logSessionStarted: vi.fn(),
|
|
152
|
+
logHookFirstUsed: vi.fn(),
|
|
153
|
+
logDevError: vi.fn(),
|
|
154
|
+
endSession: vi.fn(),
|
|
155
|
+
dispose: vi.fn(),
|
|
156
|
+
hooksUsed: new Set(),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
initTelemetry(instance, 'abc123')
|
|
160
|
+
await flushPromises()
|
|
161
|
+
|
|
162
|
+
expect(createTelemetryManager).toHaveBeenCalled()
|
|
163
|
+
const manager = vi.mocked(createTelemetryManager).mock.results[0].value
|
|
164
|
+
|
|
165
|
+
expect(manager.logSessionStarted).not.toHaveBeenCalled()
|
|
166
|
+
expect(manager.dispose).toHaveBeenCalled()
|
|
167
|
+
expect(manager.endSession).not.toHaveBeenCalled()
|
|
168
|
+
expect(getTelemetryManager(instance)).toBeUndefined()
|
|
169
|
+
|
|
170
|
+
instance.dispose()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('uses perspective from config when available', async () => {
|
|
174
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
175
|
+
|
|
176
|
+
const instance = createSanityInstance({perspective: 'previewDrafts'})
|
|
177
|
+
|
|
178
|
+
initTelemetry(instance, 'abc123')
|
|
179
|
+
await flushPromises()
|
|
180
|
+
|
|
181
|
+
const manager = vi.mocked(createTelemetryManager).mock.results[0].value
|
|
182
|
+
expect(manager.logSessionStarted).toHaveBeenCalledWith(
|
|
183
|
+
expect.objectContaining({
|
|
184
|
+
perspective: 'previewDrafts',
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
instance.dispose()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('flushes hooks buffered before manager is ready', async () => {
|
|
192
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
193
|
+
|
|
194
|
+
const instance = createSanityInstance()
|
|
195
|
+
|
|
196
|
+
trackHookMounted(instance, 'useQuery')
|
|
197
|
+
trackHookMounted(instance, 'useDocument')
|
|
198
|
+
|
|
199
|
+
initTelemetry(instance, 'abc123')
|
|
200
|
+
await flushPromises()
|
|
201
|
+
|
|
202
|
+
const manager = vi.mocked(createTelemetryManager).mock.results[0].value
|
|
203
|
+
expect(manager.logHookFirstUsed).toHaveBeenCalledWith('useQuery')
|
|
204
|
+
expect(manager.logHookFirstUsed).toHaveBeenCalledWith('useDocument')
|
|
205
|
+
|
|
206
|
+
instance.dispose()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('finds manager through parent-child instance chain', async () => {
|
|
210
|
+
vi.mocked(isDevMode).mockReturnValue(true)
|
|
211
|
+
|
|
212
|
+
const root = createSanityInstance()
|
|
213
|
+
const child = root.createChild({})
|
|
214
|
+
|
|
215
|
+
initTelemetry(root, 'abc123')
|
|
216
|
+
await flushPromises()
|
|
217
|
+
|
|
218
|
+
trackHookMounted(child, 'useUsers')
|
|
219
|
+
|
|
220
|
+
const manager = vi.mocked(createTelemetryManager).mock.results[0].value
|
|
221
|
+
expect(manager.logHookFirstUsed).toHaveBeenCalledWith('useUsers')
|
|
222
|
+
|
|
223
|
+
root.dispose()
|
|
224
|
+
})
|
|
225
|
+
})
|