@gscdump/nuxt 0.19.7 → 0.20.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/README.md
CHANGED
|
@@ -81,9 +81,10 @@ the data — e.g. nuxtseo.com pro consuming gscdump.com. Two pieces:
|
|
|
81
81
|
2. **Hand the layer a per-viewer api key**: register a client plugin that
|
|
82
82
|
trades the viewer's host session for an api key, then call
|
|
83
83
|
`setGscFetchHeaders({ 'x-api-key': key })`. The layer's `useGscFetch`
|
|
84
|
-
attaches the header to every `/api/__gsc/*` call automatically.
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
attaches the header to every `/api/__gsc/*` call automatically. Browser
|
|
85
|
+
parquet reads use exact-object URLs minted by the source endpoint; runtime
|
|
86
|
+
preflights can include this header, while DuckDB-WASM range reads are
|
|
87
|
+
authorized by the token embedded in each URL.
|
|
87
88
|
|
|
88
89
|
The `setupGscFetchAuth` helper collapses the boilerplate:
|
|
89
90
|
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
// Use this to render locked/ghost UI upfront without issuing a doomed
|
|
6
6
|
// analyze() call for each panel.
|
|
7
7
|
//
|
|
8
|
-
// The reactive composable and the non-reactive `loadSourceInfoFor`
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// The reactive composable and the non-reactive `loadSourceInfoFor` share one
|
|
9
|
+
// entry per site/searchType/range via the shared site-resource seam, so they
|
|
10
|
+
// collapse to one network read per session slice.
|
|
11
11
|
|
|
12
12
|
import type { SourceCapabilities } from '@gscdump/analysis'
|
|
13
|
+
import type { SourceInfoOptions } from '@gscdump/contracts'
|
|
13
14
|
import { acquireSharedEntry, useGscSharedSiteResource } from './_useGscSharedSiteResource'
|
|
14
15
|
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
15
16
|
|
|
@@ -48,6 +49,7 @@ interface SourceInfoEntry {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const SOURCE_INFO_NAMESPACE = 'source-info'
|
|
52
|
+
const DEFAULT_SEARCH_TYPE = 'web'
|
|
51
53
|
|
|
52
54
|
function createEntry(): SourceInfoEntry {
|
|
53
55
|
return {
|
|
@@ -58,12 +60,21 @@ function createEntry(): SourceInfoEntry {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
function
|
|
63
|
+
function sourceInfoKey(siteId: string, options?: SourceInfoOptions): string {
|
|
64
|
+
return JSON.stringify([
|
|
65
|
+
siteId,
|
|
66
|
+
options?.searchType ?? DEFAULT_SEARCH_TYPE,
|
|
67
|
+
options?.start ?? options?.startDate ?? null,
|
|
68
|
+
options?.end ?? options?.endDate ?? null,
|
|
69
|
+
])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fetchInto(entry: SourceInfoEntry, siteId: string, options?: SourceInfoOptions): Promise<void> {
|
|
62
73
|
if (entry.pending.value)
|
|
63
74
|
return entry.pending.value
|
|
64
75
|
entry.loading.value = true
|
|
65
76
|
entry.error.value = null
|
|
66
|
-
const p = (useGscAnalyticsClient().getSourceInfo(siteId) as Promise<GscAnalyticsSourceInfo>)
|
|
77
|
+
const p = (useGscAnalyticsClient().getSourceInfo(siteId, options) as Promise<GscAnalyticsSourceInfo>)
|
|
67
78
|
.then((data) => {
|
|
68
79
|
entry.info.value = data
|
|
69
80
|
})
|
|
@@ -85,11 +96,11 @@ function fetchInto(entry: SourceInfoEntry, siteId: string): Promise<void> {
|
|
|
85
96
|
* site-resource seam, so gating UI and the analyzer mode probe collapse to
|
|
86
97
|
* one network read per site per session.
|
|
87
98
|
*/
|
|
88
|
-
export async function loadSourceInfoFor(siteId: string): Promise<GscAnalyticsSourceInfo> {
|
|
89
|
-
const entry = acquireSharedEntry(SOURCE_INFO_NAMESPACE, siteId, createEntry)
|
|
99
|
+
export async function loadSourceInfoFor(siteId: string, options?: SourceInfoOptions): Promise<GscAnalyticsSourceInfo> {
|
|
100
|
+
const entry = acquireSharedEntry(SOURCE_INFO_NAMESPACE, sourceInfoKey(siteId, options), createEntry)
|
|
90
101
|
if (entry.info.value)
|
|
91
102
|
return entry.info.value
|
|
92
|
-
await fetchInto(entry, siteId)
|
|
103
|
+
await fetchInto(entry, siteId, options)
|
|
93
104
|
if (entry.error.value)
|
|
94
105
|
throw entry.error.value
|
|
95
106
|
if (!entry.info.value)
|
|
@@ -99,8 +110,13 @@ export async function loadSourceInfoFor(siteId: string): Promise<GscAnalyticsSou
|
|
|
99
110
|
|
|
100
111
|
export function useGscAnalyticsSourceInfo(
|
|
101
112
|
siteId: MaybeRefOrGetter<string | null | undefined>,
|
|
113
|
+
options: MaybeRefOrGetter<SourceInfoOptions | null | undefined> = null,
|
|
102
114
|
): GscAnalyticsSourceInfoState {
|
|
103
|
-
const
|
|
115
|
+
const cacheKey = computed(() => {
|
|
116
|
+
const id = toValue(siteId)
|
|
117
|
+
return id ? sourceInfoKey(id, toValue(options) ?? undefined) : null
|
|
118
|
+
})
|
|
119
|
+
const { bound } = useGscSharedSiteResource<SourceInfoEntry>(SOURCE_INFO_NAMESPACE, cacheKey, {
|
|
104
120
|
factory: () => createEntry(),
|
|
105
121
|
// No onDispose: source-info is cheap and persistent across the session.
|
|
106
122
|
})
|
|
@@ -113,7 +129,7 @@ export function useGscAnalyticsSourceInfo(
|
|
|
113
129
|
if (!id)
|
|
114
130
|
return
|
|
115
131
|
if (entry.info.value == null && entry.pending.value == null && entry.error.value == null)
|
|
116
|
-
void fetchInto(entry, id)
|
|
132
|
+
void fetchInto(entry, id, toValue(options) ?? undefined)
|
|
117
133
|
}, { immediate: true })
|
|
118
134
|
}
|
|
119
135
|
|
|
@@ -127,7 +143,7 @@ export function useGscAnalyticsSourceInfo(
|
|
|
127
143
|
if (!entry || !id)
|
|
128
144
|
return
|
|
129
145
|
entry.info.value = null
|
|
130
|
-
await fetchInto(entry, id)
|
|
146
|
+
await fetchInto(entry, id, toValue(options) ?? undefined)
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
function supports(analyzerId: MaybeRefOrGetter<string>): ComputedRef<boolean> {
|
|
@@ -21,7 +21,6 @@ import { useGscSharedSiteResource } from './_useGscSharedSiteResource'
|
|
|
21
21
|
import { useGscAnalyticsContext } from './useGscAnalytics'
|
|
22
22
|
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
23
23
|
import { useGscAnalyticsConfig } from './useGscAnalyticsConfig'
|
|
24
|
-
import { loadSourceInfoFor } from './useGscAnalyticsSourceInfo'
|
|
25
24
|
import { resolveGscAuthHeaders } from './useGscAuth'
|
|
26
25
|
|
|
27
26
|
export interface GscAnalyzerTimings {
|
|
@@ -51,6 +50,58 @@ export interface GscAnalyzerInstance {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
const EMPTY_TABLES: readonly string[] = Object.freeze([])
|
|
53
|
+
const DEFAULT_SEARCH_TYPE: NonNullable<AnalysisParams['searchType']> = 'web'
|
|
54
|
+
const DEFAULT_ATTACH_FETCH_CONCURRENCY = 2
|
|
55
|
+
const DEFAULT_ATTACH_MAX_FILES = 32
|
|
56
|
+
const DEFAULT_ATTACH_MAX_BYTES = 16 * 1024 * 1024
|
|
57
|
+
|
|
58
|
+
interface AnalyzerRange {
|
|
59
|
+
start: string
|
|
60
|
+
end: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface AnalysisSourcesClient {
|
|
64
|
+
getAnalysisSources: (
|
|
65
|
+
siteId: string,
|
|
66
|
+
tables?: string[] | string | { tables?: string[] | string, searchType?: NonNullable<AnalysisParams['searchType']>, start?: string, end?: string },
|
|
67
|
+
options?: { searchType?: NonNullable<AnalysisParams['searchType']>, start?: string, end?: string },
|
|
68
|
+
) => Promise<AnalysisSourcesResponse>
|
|
69
|
+
getSourceInfo: (
|
|
70
|
+
siteId: string,
|
|
71
|
+
options?: { searchType?: NonNullable<AnalysisParams['searchType']>, start?: string, end?: string },
|
|
72
|
+
) => Promise<SourceInfoResponse>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeSearchType(searchType: AnalysisParams['searchType']): NonNullable<AnalysisParams['searchType']> {
|
|
76
|
+
return searchType ?? DEFAULT_SEARCH_TYPE
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function analyzerCacheKey(siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): string {
|
|
80
|
+
return JSON.stringify([siteId, searchType, range?.start ?? null, range?.end ?? null])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseAnalyzerCacheKey(key: string): { siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null } {
|
|
84
|
+
const [siteId, searchType, start, end] = JSON.parse(key) as [string, NonNullable<AnalysisParams['searchType']>, string | null, string | null]
|
|
85
|
+
return { siteId, searchType, range: start && end ? { start, end } : null }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeRange(range: AnalyzerRange | null | undefined): AnalyzerRange | null {
|
|
89
|
+
return range?.start && range?.end ? range : null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadSourceInfo(siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<SourceInfoResponse> {
|
|
93
|
+
return (useGscAnalyticsClient() as unknown as AnalysisSourcesClient).getSourceInfo(siteId, {
|
|
94
|
+
searchType,
|
|
95
|
+
...(range ? { start: range.start, end: range.end } : {}),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function loadAnalysisSources(siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<AnalysisSourcesResponse> {
|
|
100
|
+
return (useGscAnalyticsClient() as unknown as AnalysisSourcesClient).getAnalysisSources(siteId, undefined, {
|
|
101
|
+
searchType,
|
|
102
|
+
...(range ? { start: range.start, end: range.end } : {}),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
54
105
|
|
|
55
106
|
/**
|
|
56
107
|
* Get (or create) an analyzer for a site. Per-site cached across the app so
|
|
@@ -59,10 +110,21 @@ const EMPTY_TABLES: readonly string[] = Object.freeze([])
|
|
|
59
110
|
* bound cached instance; switching `siteId` rebinds and the computeds track
|
|
60
111
|
* the new instance with no manual mirroring.
|
|
61
112
|
*/
|
|
62
|
-
export function useGscAnalyzer(
|
|
113
|
+
export function useGscAnalyzer(
|
|
114
|
+
siteId: MaybeRefOrGetter<string | null | undefined>,
|
|
115
|
+
searchType: MaybeRefOrGetter<AnalysisParams['searchType']> = DEFAULT_SEARCH_TYPE,
|
|
116
|
+
range: MaybeRefOrGetter<AnalyzerRange | null | undefined> = null,
|
|
117
|
+
): GscAnalyzerInstance & { currentSiteId: Ref<string | null> } {
|
|
63
118
|
const ctx = useGscAnalyticsContext()
|
|
64
|
-
const
|
|
65
|
-
|
|
119
|
+
const cacheKey = computed(() => {
|
|
120
|
+
const id = toValue(siteId)
|
|
121
|
+
return id ? analyzerCacheKey(id, normalizeSearchType(toValue(searchType)), normalizeRange(toValue(range))) : null
|
|
122
|
+
})
|
|
123
|
+
const { bound, currentSiteId: currentCacheKey } = useGscSharedSiteResource<GscAnalyzerInstance>('analyzer', cacheKey, {
|
|
124
|
+
factory: (key) => {
|
|
125
|
+
const parsed = parseAnalyzerCacheKey(key)
|
|
126
|
+
return createInstance(parsed.siteId, parsed.searchType, parsed.range, ctx.patchProgress)
|
|
127
|
+
},
|
|
66
128
|
onDispose: inst => inst.dispose(),
|
|
67
129
|
})
|
|
68
130
|
|
|
@@ -74,6 +136,10 @@ export function useGscAnalyzer(siteId: MaybeRefOrGetter<string | null | undefine
|
|
|
74
136
|
const attachedTables = computed(() => bound.value?.attachedTables.value ?? (EMPTY_TABLES as string[])) as unknown as Ref<string[]>
|
|
75
137
|
const timings = computed(() => bound.value?.timings.value ?? null) as unknown as Ref<GscAnalyzerTimings | null>
|
|
76
138
|
const manifestVersion = computed(() => bound.value?.manifestVersion.value) as unknown as Ref<string | undefined>
|
|
139
|
+
const currentSiteId = computed(() => {
|
|
140
|
+
const key = currentCacheKey.value
|
|
141
|
+
return key ? parseAnalyzerCacheKey(key).siteId : null
|
|
142
|
+
}) as unknown as Ref<string | null>
|
|
77
143
|
|
|
78
144
|
async function query(sql: string, params?: unknown[]): Promise<QueryResult> {
|
|
79
145
|
const inst = bound.value
|
|
@@ -115,8 +181,9 @@ export function useGscAnalyzer(siteId: MaybeRefOrGetter<string | null | undefine
|
|
|
115
181
|
|
|
116
182
|
function createInstance(
|
|
117
183
|
siteId: string,
|
|
184
|
+
searchType: NonNullable<AnalysisParams['searchType']>,
|
|
185
|
+
range: AnalyzerRange | null,
|
|
118
186
|
patchProgress: (id: string, p: Partial<SiteLoadProgress>) => void,
|
|
119
|
-
sourceInfoLoader: () => Promise<SourceInfoResponse>,
|
|
120
187
|
): GscAnalyzerInstance {
|
|
121
188
|
const ready = ref(false)
|
|
122
189
|
const initializing = ref(true)
|
|
@@ -133,6 +200,7 @@ function createInstance(
|
|
|
133
200
|
let bootedDb: DuckDBWasmBootResult | null = null
|
|
134
201
|
let attachedHandle: AttachedTablesHandle | null = null
|
|
135
202
|
let mode: 'browser-attached' | 'server' = 'server'
|
|
203
|
+
const lifetimeController = new AbortController()
|
|
136
204
|
const inFlight = new Map<string, Promise<AnalysisResult & { queryMs: number }>>()
|
|
137
205
|
|
|
138
206
|
// Cross-origin: if the host returns relative parquet URLs (`/api/r2-data/…`)
|
|
@@ -146,9 +214,11 @@ function createInstance(
|
|
|
146
214
|
return `${apiBase.replace(/\/+$/, '')}${url}`
|
|
147
215
|
}
|
|
148
216
|
|
|
149
|
-
async function attachFromSources(sources: AnalysisSourcesResponse): Promise<{ attached: number, total: number }> {
|
|
217
|
+
async function attachFromSources(sources: AnalysisSourcesResponse, signal?: AbortSignal): Promise<{ attached: number, total: number }> {
|
|
150
218
|
if (!bootedDb)
|
|
151
219
|
throw new Error('useGscAnalyzer: attachFromSources called before DuckDB boot')
|
|
220
|
+
if (sources.canUseBrowser === false)
|
|
221
|
+
throw new Error(`useGscAnalyzer: browser attach unavailable: ${sources.reason ?? sources.fallback ?? 'coverage plan rejected'}`)
|
|
152
222
|
|
|
153
223
|
const tables = Object.entries(sources.tables)
|
|
154
224
|
.filter(([, urls]) => Array.isArray(urls) && urls.length > 0)
|
|
@@ -157,11 +227,10 @@ function createInstance(
|
|
|
157
227
|
const total = tables.reduce((n, t) => n + t.urls.length, 0)
|
|
158
228
|
patch({ stage: 'attach', filesTotal: total, filesAttached: 0 })
|
|
159
229
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// different session realms when the consumer mode is active.
|
|
230
|
+
// URL preflights can use the same host-supplied auth header as /api/__gsc/*.
|
|
231
|
+
// The actual DuckDB range reads are authorized by the exact-key token
|
|
232
|
+
// embedded in each analysis-sources URL, because registerFileURL cannot
|
|
233
|
+
// carry custom fetch headers into DuckDB-WASM's internal HTTP reader.
|
|
165
234
|
const extraHeaders = resolveGscAuthHeaders()
|
|
166
235
|
const hasExtra = Object.keys(extraHeaders).length > 0
|
|
167
236
|
let attached = 0
|
|
@@ -174,6 +243,10 @@ function createInstance(
|
|
|
174
243
|
fetchInit: hasExtra
|
|
175
244
|
? { credentials: 'omit', headers: extraHeaders }
|
|
176
245
|
: { credentials: 'same-origin' },
|
|
246
|
+
fetchConcurrency: DEFAULT_ATTACH_FETCH_CONCURRENCY,
|
|
247
|
+
maxFiles: DEFAULT_ATTACH_MAX_FILES,
|
|
248
|
+
maxBytes: DEFAULT_ATTACH_MAX_BYTES,
|
|
249
|
+
signal,
|
|
177
250
|
onFileAttached: () => {
|
|
178
251
|
attached++
|
|
179
252
|
patch({ filesAttached: attached })
|
|
@@ -193,7 +266,7 @@ function createInstance(
|
|
|
193
266
|
patch({ stage: 'manifest', startedAt: Date.now(), filesAttached: 0, filesTotal: 0, error: undefined, endedAt: undefined })
|
|
194
267
|
// Probe the server-resolved source first. Its kind + attachedTables bit
|
|
195
268
|
// decides whether we boot DuckDB-WASM (expensive) or proxy to the server.
|
|
196
|
-
const info = await
|
|
269
|
+
const info = await loadSourceInfo(siteId, searchType, range)
|
|
197
270
|
mode = info.browserAttachEligible ? 'browser-attached' : 'server'
|
|
198
271
|
|
|
199
272
|
if (mode === 'server') {
|
|
@@ -224,11 +297,11 @@ function createInstance(
|
|
|
224
297
|
|
|
225
298
|
patch({ stage: 'manifest' })
|
|
226
299
|
const t1 = performance.now()
|
|
227
|
-
const sources = await
|
|
300
|
+
const sources = await loadAnalysisSources(siteId, searchType, range)
|
|
228
301
|
const manifestMs = performance.now() - t1
|
|
229
302
|
|
|
230
303
|
const t2 = performance.now()
|
|
231
|
-
const { total } = await attachFromSources(sources)
|
|
304
|
+
const { total } = await attachFromSources(sources, lifetimeController.signal)
|
|
232
305
|
const attachMs = performance.now() - t2
|
|
233
306
|
|
|
234
307
|
runtime = createBrowserAnalysisRuntime(bootedDb, { schema: 'main', attachedTables: attachedTables.value })
|
|
@@ -259,7 +332,7 @@ function createInstance(
|
|
|
259
332
|
}
|
|
260
333
|
|
|
261
334
|
async function runServerAnalyze(params: AnalysisParams, _signal?: AbortSignal): Promise<AnalysisResult & { queryMs: number }> {
|
|
262
|
-
const out = await useGscAnalyticsClient().analyze<AnalysisResult & { queryMs?: number }>(siteId, params)
|
|
335
|
+
const out = await useGscAnalyticsClient().analyze<AnalysisResult & { queryMs?: number }>(siteId, { ...params, searchType: params.searchType ?? searchType })
|
|
263
336
|
return {
|
|
264
337
|
results: coerceResults(out.results) as AnalysisResult['results'],
|
|
265
338
|
meta: out.meta as AnalysisResult['meta'],
|
|
@@ -268,18 +341,19 @@ function createInstance(
|
|
|
268
341
|
}
|
|
269
342
|
|
|
270
343
|
async function analyze(params: AnalysisParams, opts?: { signal?: AbortSignal }): Promise<AnalysisResult & { queryMs: number }> {
|
|
271
|
-
const rt = await boot
|
|
344
|
+
const rt = opts?.signal ? await raceSignal(boot, opts.signal) : await boot
|
|
272
345
|
opts?.signal?.throwIfAborted?.()
|
|
273
346
|
|
|
274
|
-
const
|
|
347
|
+
const scopedParams = { ...params, searchType: params.searchType ?? searchType }
|
|
348
|
+
const key = JSON.stringify({ manifestVersion: manifestVersion.value, params: scopedParams, searchType, siteId })
|
|
275
349
|
const existing = inFlight.get(key)
|
|
276
350
|
if (existing)
|
|
277
351
|
return opts?.signal ? raceSignal(existing, opts.signal) : existing
|
|
278
352
|
|
|
279
353
|
const p = (async () => {
|
|
280
354
|
if (!rt)
|
|
281
|
-
return runServerAnalyze(
|
|
282
|
-
const out = await rt.analyze(
|
|
355
|
+
return runServerAnalyze(scopedParams, opts?.signal)
|
|
356
|
+
const out = await rt.analyze(scopedParams as never, defaultAnalyzerRegistry, { signal: opts?.signal })
|
|
283
357
|
opts?.signal?.throwIfAborted?.()
|
|
284
358
|
return {
|
|
285
359
|
results: coerceResults(out.results) as AnalysisResult['results'],
|
|
@@ -287,19 +361,22 @@ function createInstance(
|
|
|
287
361
|
queryMs: out.queryMs,
|
|
288
362
|
}
|
|
289
363
|
})()
|
|
364
|
+
if (opts?.signal) {
|
|
365
|
+
return raceSignal(p, opts.signal)
|
|
366
|
+
}
|
|
290
367
|
inFlight.set(key, p)
|
|
291
368
|
p.finally(() => {
|
|
292
369
|
if (inFlight.get(key) === p)
|
|
293
370
|
inFlight.delete(key)
|
|
294
371
|
})
|
|
295
|
-
return
|
|
372
|
+
return p
|
|
296
373
|
}
|
|
297
374
|
|
|
298
375
|
async function refresh(): Promise<boolean> {
|
|
299
376
|
await boot
|
|
300
377
|
if (mode !== 'browser-attached' || !runtime || !bootedDb)
|
|
301
378
|
return false
|
|
302
|
-
const sources = await
|
|
379
|
+
const sources = await loadAnalysisSources(siteId, searchType, range)
|
|
303
380
|
if (!runtime.isStale(sources.manifestVersion))
|
|
304
381
|
return false
|
|
305
382
|
// Drop the stale views before swapping in the new partitions. The runtime
|
|
@@ -315,6 +392,7 @@ function createInstance(
|
|
|
315
392
|
}
|
|
316
393
|
|
|
317
394
|
async function dispose(): Promise<void> {
|
|
395
|
+
lifetimeController.abort()
|
|
318
396
|
await runtime?.close().catch((e) => {
|
|
319
397
|
console.error('[analyzer] runtime.close failed', e)
|
|
320
398
|
})
|
|
@@ -132,8 +132,21 @@ async function defaultServerFallback<T>(siteId: string, params: AnalysisParams):
|
|
|
132
132
|
return await useGscAnalyticsClient().analyze<T>(siteId, params)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
const DEFAULT_SEARCH_TYPE: NonNullable<AnalysisParams['searchType']> = 'web'
|
|
136
|
+
|
|
137
|
+
function withDefaultSearchType(params: AnalysisParams): AnalysisParams {
|
|
138
|
+
return { ...params, searchType: params.searchType ?? DEFAULT_SEARCH_TYPE }
|
|
139
|
+
}
|
|
140
|
+
|
|
135
141
|
export function useGscQuery<T = AnalysisResult>(opts: UseGscQueryOptions<T>): UseGscQueryReturn<T> {
|
|
136
|
-
const
|
|
142
|
+
const querySearchType = computed(() => toValue(opts.params).searchType ?? DEFAULT_SEARCH_TYPE)
|
|
143
|
+
const queryRange = computed(() => {
|
|
144
|
+
const params = toValue(opts.params)
|
|
145
|
+
return typeof params.startDate === 'string' && typeof params.endDate === 'string'
|
|
146
|
+
? { start: params.startDate, end: params.endDate }
|
|
147
|
+
: null
|
|
148
|
+
})
|
|
149
|
+
const analyzer = useGscAnalyzer(opts.site, querySearchType, queryRange)
|
|
137
150
|
const dispatcher = useGscQueryDispatcher()
|
|
138
151
|
|
|
139
152
|
const data = shallowRef<T | null>(null)
|
|
@@ -164,7 +177,7 @@ export function useGscQuery<T = AnalysisResult>(opts: UseGscQueryOptions<T>): Us
|
|
|
164
177
|
async function runServer(siteId: string): Promise<void> {
|
|
165
178
|
const t0 = performance.now()
|
|
166
179
|
const fn = opts.serverFallback ?? defaultServerFallback
|
|
167
|
-
const out = await fn(siteId, toValue(opts.params)) as T
|
|
180
|
+
const out = await fn(siteId, withDefaultSearchType(toValue(opts.params))) as T
|
|
168
181
|
data.value = out
|
|
169
182
|
engine.value = 'server'
|
|
170
183
|
elapsedMs.value = performance.now() - t0
|
|
@@ -172,7 +185,7 @@ export function useGscQuery<T = AnalysisResult>(opts: UseGscQueryOptions<T>): Us
|
|
|
172
185
|
}
|
|
173
186
|
|
|
174
187
|
async function runBrowser(signal: AbortSignal): Promise<void> {
|
|
175
|
-
const out = await analyzer.analyze(toValue(opts.params), { signal })
|
|
188
|
+
const out = await analyzer.analyze(withDefaultSearchType(toValue(opts.params)), { signal })
|
|
176
189
|
signal.throwIfAborted()
|
|
177
190
|
const shaped = opts.reshape ? opts.reshape(out) : (out as unknown as T)
|
|
178
191
|
data.value = shaped
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/nuxt",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.0",
|
|
5
5
|
"description": "Nuxt layer: GSC analytics UI + DuckDB-WASM integration. Frontend-only; server primitives live in @gscdump/analysis, @gscdump/engine-sqlite, @gscdump/cloudflare, and host apps.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"defu": "^6.1.7",
|
|
56
56
|
"ofetch": "^1.5.1",
|
|
57
57
|
"tailwindcss": "^4.3.0",
|
|
58
|
-
"@gscdump/
|
|
59
|
-
"@gscdump/engine": "0.
|
|
60
|
-
"@gscdump/
|
|
61
|
-
"
|
|
62
|
-
"gscdump": "0.
|
|
63
|
-
"@gscdump/
|
|
58
|
+
"@gscdump/engine": "0.20.0",
|
|
59
|
+
"@gscdump/engine-duckdb-wasm": "0.20.0",
|
|
60
|
+
"@gscdump/sdk": "0.20.0",
|
|
61
|
+
"gscdump": "0.20.0",
|
|
62
|
+
"@gscdump/analysis": "0.20.0",
|
|
63
|
+
"@gscdump/contracts": "0.20.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@nuxt/kit": "^4.4.6",
|