@gscdump/nuxt 0.21.3 → 0.22.4
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 +36 -27
- package/app/components/GscDashboardPage.vue +2 -2
- package/app/components/GscHero.vue +3 -4
- package/app/components/GscLazyTopResult.vue +7 -5
- package/app/components/GscPerformanceChart.vue +4 -4
- package/app/components/GscSourceDebugPanel.vue +2 -1
- package/app/composables/_useGscBackfill.ts +13 -5
- package/app/composables/_useGscQueryDispatcher.ts +5 -4
- package/app/composables/_useGscResource.ts +144 -56
- package/app/composables/useGscAnalytics.ts +13 -4
- package/app/composables/useGscAnalyticsSourceInfo.ts +11 -5
- package/app/composables/useGscAnalyzer.ts +33 -25
- package/app/composables/useGscAnalyzerBatch.ts +2 -2
- package/app/composables/useGscAnalyzerDefs.ts +2 -1
- package/app/composables/useGscCountries.ts +5 -6
- package/app/composables/useGscCurrentSite.ts +32 -11
- package/app/composables/useGscInspectionHistory.ts +5 -5
- package/app/composables/useGscInspections.ts +5 -5
- package/app/composables/useGscPanelContext.ts +9 -2
- package/app/composables/useGscQuery.ts +6 -8
- package/app/composables/useGscQueryDispatcher.ts +14 -122
- package/app/composables/useGscRequestIndexingInspect.ts +20 -11
- package/app/composables/useGscResource.ts +11 -114
- package/app/composables/useGscRollup.ts +66 -81
- package/app/composables/useGscSearchAppearance.ts +5 -6
- package/app/composables/useGscSiteAnalyzer.ts +456 -0
- package/app/composables/useGscSitemapHistory.ts +5 -5
- package/app/composables/useGscSitemaps.ts +5 -5
- package/app/queries/gsc.ts +163 -0
- package/app/utils/duckdb-wasm.ts +166 -0
- package/app/utils/gsc-rpc.ts +18 -0
- package/module.ts +1 -1
- package/nuxt.config.ts +25 -0
- package/package.json +140 -8
- package/types.ts +8 -7
- package/app/composables/useGscEngine.ts +0 -16
- package/app/composables/useGscParquetTable.ts +0 -199
- package/app/composables/useGscRollupTable.ts +0 -78
- package/app/composables/useGscRowQuery.ts +0 -56
- package/app/composables/useGscSnapshotAnalyzer.contract.ts +0 -150
- package/app/composables/useGscSnapshotAnalyzer.routing.ts +0 -87
- package/app/composables/useGscSnapshotAnalyzer.ts +0 -559
- package/app/utils/setup-gsc-fetch-auth.ts +0 -62
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
// re-boot. Writes progress to the shared map so <GscBootProgress> lights up
|
|
12
12
|
// on boot regardless of mode.
|
|
13
13
|
|
|
14
|
-
import type { AnalysisParams, AnalysisResult } from '@gscdump/analysis'
|
|
15
14
|
import type { AnalysisSourcesResponse, SourceInfoResponse } from '@gscdump/contracts'
|
|
16
15
|
import type { AttachedTablesHandle, BrowserAnalysisRuntime, DuckDBWasmBootResult, QueryResult } from '@gscdump/engine-duckdb-wasm'
|
|
16
|
+
import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
|
|
17
|
+
import type { AnalyzerRegistry } from '@gscdump/engine/analyzer'
|
|
17
18
|
import type { SiteLoadProgress } from './useGscAnalytics'
|
|
18
|
-
import { defaultAnalyzerRegistry } from '@gscdump/analysis'
|
|
19
19
|
import { coerceRow } from '@gscdump/engine'
|
|
20
|
+
import { gscQueries } from '../queries/gsc'
|
|
21
|
+
import { useGscRpc } from '../utils/gsc-rpc'
|
|
20
22
|
import { useGscSharedSiteResource } from './_useGscSharedSiteResource'
|
|
21
23
|
import { useGscAnalyticsContext } from './useGscAnalytics'
|
|
22
24
|
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
@@ -55,23 +57,13 @@ const DEFAULT_ATTACH_FETCH_CONCURRENCY = 2
|
|
|
55
57
|
const DEFAULT_ATTACH_MAX_FILES = 32
|
|
56
58
|
const DEFAULT_ATTACH_MAX_BYTES = 16 * 1024 * 1024
|
|
57
59
|
|
|
60
|
+
let defaultAnalyzerRegistryPromise: Promise<AnalyzerRegistry> | null = null
|
|
61
|
+
|
|
58
62
|
interface AnalyzerRange {
|
|
59
63
|
start: string
|
|
60
64
|
end: string
|
|
61
65
|
}
|
|
62
66
|
|
|
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
67
|
function normalizeSearchType(searchType: AnalysisParams['searchType']): NonNullable<AnalysisParams['searchType']> {
|
|
76
68
|
return searchType ?? DEFAULT_SEARCH_TYPE
|
|
77
69
|
}
|
|
@@ -89,18 +81,26 @@ function normalizeRange(range: AnalyzerRange | null | undefined): AnalyzerRange
|
|
|
89
81
|
return range?.start && range?.end ? range : null
|
|
90
82
|
}
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
type AnalyticsClient = ReturnType<typeof useGscAnalyticsClient>
|
|
85
|
+
|
|
86
|
+
function loadSourceInfo(client: AnalyticsClient, siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<SourceInfoResponse> {
|
|
87
|
+
return client.getSourceInfo(siteId, {
|
|
94
88
|
searchType,
|
|
95
89
|
...(range ? { start: range.start, end: range.end } : {}),
|
|
96
|
-
})
|
|
90
|
+
}) as Promise<SourceInfoResponse>
|
|
97
91
|
}
|
|
98
92
|
|
|
99
|
-
function loadAnalysisSources(siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<AnalysisSourcesResponse> {
|
|
100
|
-
return
|
|
93
|
+
function loadAnalysisSources(client: AnalyticsClient, siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<AnalysisSourcesResponse> {
|
|
94
|
+
return client.getAnalysisSources(siteId, undefined, {
|
|
101
95
|
searchType,
|
|
102
96
|
...(range ? { start: range.start, end: range.end } : {}),
|
|
103
|
-
})
|
|
97
|
+
}) as Promise<AnalysisSourcesResponse>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function loadDefaultAnalyzerRegistry(): Promise<AnalyzerRegistry> {
|
|
101
|
+
defaultAnalyzerRegistryPromise ??= import('@gscdump/analysis/registry')
|
|
102
|
+
.then(m => m.defaultAnalyzerRegistry)
|
|
103
|
+
return defaultAnalyzerRegistryPromise
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
@@ -191,6 +191,8 @@ function createInstance(
|
|
|
191
191
|
const attachedTables = ref<string[]>([])
|
|
192
192
|
const timings = ref<GscAnalyzerTimings | null>(null)
|
|
193
193
|
const manifestVersion = ref<string | undefined>(undefined)
|
|
194
|
+
const client = useGscAnalyticsClient()
|
|
195
|
+
const rpc = useGscRpc()
|
|
194
196
|
|
|
195
197
|
function patch(p: Partial<SiteLoadProgress>): void {
|
|
196
198
|
patchProgress(siteId, { source: 'duckdb', ...p })
|
|
@@ -266,7 +268,7 @@ function createInstance(
|
|
|
266
268
|
patch({ stage: 'manifest', startedAt: Date.now(), filesAttached: 0, filesTotal: 0, error: undefined, endedAt: undefined })
|
|
267
269
|
// Probe the server-resolved source first. Its kind + attachedTables bit
|
|
268
270
|
// decides whether we boot DuckDB-WASM (expensive) or proxy to the server.
|
|
269
|
-
const info = await loadSourceInfo(siteId, searchType, range)
|
|
271
|
+
const info = await loadSourceInfo(client, siteId, searchType, range)
|
|
270
272
|
mode = info.browserAttachEligible ? 'browser-attached' : 'server'
|
|
271
273
|
|
|
272
274
|
if (mode === 'server') {
|
|
@@ -297,7 +299,7 @@ function createInstance(
|
|
|
297
299
|
|
|
298
300
|
patch({ stage: 'manifest' })
|
|
299
301
|
const t1 = performance.now()
|
|
300
|
-
const sources = await loadAnalysisSources(siteId, searchType, range)
|
|
302
|
+
const sources = await loadAnalysisSources(client, siteId, searchType, range)
|
|
301
303
|
const manifestMs = performance.now() - t1
|
|
302
304
|
|
|
303
305
|
const t2 = performance.now()
|
|
@@ -332,7 +334,11 @@ function createInstance(
|
|
|
332
334
|
}
|
|
333
335
|
|
|
334
336
|
async function runServerAnalyze(params: AnalysisParams, _signal?: AbortSignal): Promise<AnalysisResult & { queryMs: number }> {
|
|
335
|
-
const out = await
|
|
337
|
+
const out = await rpc.execute(
|
|
338
|
+
gscQueries.analyze(siteId),
|
|
339
|
+
{ ...params, searchType: params.searchType ?? searchType } as never,
|
|
340
|
+
{ silent: true },
|
|
341
|
+
) as AnalysisResult & { queryMs?: number }
|
|
336
342
|
return {
|
|
337
343
|
results: coerceResults(out.results) as AnalysisResult['results'],
|
|
338
344
|
meta: out.meta as AnalysisResult['meta'],
|
|
@@ -353,7 +359,9 @@ function createInstance(
|
|
|
353
359
|
const p = (async () => {
|
|
354
360
|
if (!rt)
|
|
355
361
|
return runServerAnalyze(scopedParams, opts?.signal)
|
|
356
|
-
const
|
|
362
|
+
const registry = await loadDefaultAnalyzerRegistry()
|
|
363
|
+
opts?.signal?.throwIfAborted?.()
|
|
364
|
+
const out = await rt.analyze(scopedParams as never, registry, { signal: opts?.signal })
|
|
357
365
|
opts?.signal?.throwIfAborted?.()
|
|
358
366
|
return {
|
|
359
367
|
results: coerceResults(out.results) as AnalysisResult['results'],
|
|
@@ -376,7 +384,7 @@ function createInstance(
|
|
|
376
384
|
await boot
|
|
377
385
|
if (mode !== 'browser-attached' || !runtime || !bootedDb)
|
|
378
386
|
return false
|
|
379
|
-
const sources = await loadAnalysisSources(siteId, searchType, range)
|
|
387
|
+
const sources = await loadAnalysisSources(client, siteId, searchType, range)
|
|
380
388
|
if (!runtime.isStale(sources.manifestVersion))
|
|
381
389
|
return false
|
|
382
390
|
// Drop the stale views before swapping in the new partitions. The runtime
|
|
@@ -16,7 +16,7 @@ export interface GscAnalyzerBatchEntry<TResult> {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface GscAnalyzerBatchRunner {
|
|
19
|
-
analyze: (params: { type: string,
|
|
19
|
+
analyze: (params: { type: string, startDate?: string, endDate?: string }) => Promise<unknown>
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface UseGscAnalyzerBatchOptions {
|
|
@@ -72,7 +72,7 @@ export function useGscAnalyzerBatch<TResult = unknown>(
|
|
|
72
72
|
return
|
|
73
73
|
states.value = { ...states.value, [id]: { status: 'running', result: null, error: null } }
|
|
74
74
|
try {
|
|
75
|
-
const result = await runner.analyze({ type: id,
|
|
75
|
+
const result = await runner.analyze({ type: id, startDate: range.start, endDate: range.end })
|
|
76
76
|
if (token !== myToken)
|
|
77
77
|
return
|
|
78
78
|
states.value = { ...states.value, [id]: { status: 'done', result: result as TResult, error: null } }
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// `ActionSource` union from `@gscdump/analysis`, so misspelt or unknown
|
|
13
13
|
// sources fail at typecheck instead of at runtime in the priority runner.
|
|
14
14
|
|
|
15
|
-
import type { ActionSource
|
|
15
|
+
import type { ActionSource } from '@gscdump/analysis'
|
|
16
|
+
import type { AnalysisTool } from '@gscdump/engine/analysis-types'
|
|
16
17
|
import type { Component } from '@vue/runtime-core'
|
|
17
18
|
|
|
18
19
|
export type GscAnalyzerKind = 'analyzer' | 'semantic' | 'action'
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { CountriesResponse, CountryRow, GscApiRange } from '@gscdump/contracts'
|
|
6
6
|
import type { ComputedRef, Ref } from '@vue/runtime-core'
|
|
7
7
|
import type { GscResourceStatus } from './_useGscResource'
|
|
8
|
+
import { gscQueries } from '../queries/gsc'
|
|
8
9
|
import { useGscResource } from './_useGscResource'
|
|
9
|
-
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
10
10
|
|
|
11
11
|
export interface UseGscCountriesReturn {
|
|
12
12
|
response: Readonly<Ref<CountriesResponse | null>>
|
|
@@ -14,8 +14,8 @@ export interface UseGscCountriesReturn {
|
|
|
14
14
|
range: ComputedRef<GscApiRange | null>
|
|
15
15
|
source: ComputedRef<CountriesResponse['source'] | null>
|
|
16
16
|
loading: ComputedRef<boolean>
|
|
17
|
-
status: Ref<GscResourceStatus
|
|
18
|
-
error: Ref<Error | null
|
|
17
|
+
status: Readonly<Ref<GscResourceStatus>>
|
|
18
|
+
error: Readonly<Ref<Error | null>>
|
|
19
19
|
refresh: () => Promise<void>
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -23,14 +23,13 @@ export function useGscCountries(
|
|
|
23
23
|
siteId: MaybeRefOrGetter<string | null | undefined>,
|
|
24
24
|
range: MaybeRefOrGetter<{ start: string, end: string } | null | undefined>,
|
|
25
25
|
): UseGscCountriesReturn {
|
|
26
|
-
const { data, status, loading, error, refresh } = useGscResource({
|
|
26
|
+
const { data, status, loading, error, refresh } = useGscResource<[string, string, string], CountriesResponse>({
|
|
27
27
|
keys: [
|
|
28
28
|
siteId,
|
|
29
29
|
() => toValue(range)?.start,
|
|
30
30
|
() => toValue(range)?.end,
|
|
31
31
|
] as const,
|
|
32
|
-
|
|
33
|
-
useGscAnalyticsClient().getCountries(id, { start, end }),
|
|
32
|
+
operation: (id, start, end) => gscQueries.countries(id, { start, end }),
|
|
34
33
|
isEmpty: r => r.rows.length === 0,
|
|
35
34
|
})
|
|
36
35
|
|
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// The analytics layer is otherwise route-agnostic — this composable is the
|
|
5
|
-
// one place the `id` param name is hard-coded. Hosts using a different param
|
|
6
|
-
// name should provide their own equivalent.
|
|
7
|
-
|
|
8
|
-
export function useGscCurrentSite(): {
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface GscCurrentSiteContext {
|
|
9
4
|
siteId: ComputedRef<string>
|
|
10
5
|
site: ReturnType<typeof useGscSite>
|
|
11
|
-
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const gscCurrentSiteKey: InjectionKey<GscCurrentSiteContext> = Symbol('gsc-current-site')
|
|
9
|
+
|
|
10
|
+
function createGscCurrentSiteContext(siteId: MaybeRefOrGetter<string | null | undefined>): GscCurrentSiteContext {
|
|
11
|
+
const resolvedSiteId = computed(() => String(toValue(siteId) ?? ''))
|
|
12
|
+
return { siteId: resolvedSiteId, site: useGscSite(resolvedSiteId) }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provide the current-site context for a dashboard subtree. Hosts with a route
|
|
17
|
+
* shape other than `/sites/[id]/*` should call this from their layout/page.
|
|
18
|
+
*/
|
|
19
|
+
export function provideGscCurrentSite(siteId: MaybeRefOrGetter<string | null | undefined>): GscCurrentSiteContext {
|
|
20
|
+
const ctx = createGscCurrentSiteContext(siteId)
|
|
21
|
+
provide(gscCurrentSiteKey, ctx)
|
|
22
|
+
return ctx
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read the current-site context. Falls back to the historical `/sites/[id]/*`
|
|
27
|
+
* route convention for existing hosts that have not added a provider yet.
|
|
28
|
+
*/
|
|
29
|
+
export function useGscCurrentSite(): GscCurrentSiteContext {
|
|
30
|
+
const provided = inject(gscCurrentSiteKey, null)
|
|
31
|
+
if (provided)
|
|
32
|
+
return provided
|
|
33
|
+
|
|
12
34
|
const route = useRoute()
|
|
13
|
-
|
|
14
|
-
return { siteId, site: useGscSite(siteId) }
|
|
35
|
+
return createGscCurrentSiteContext(() => route.params.id as string | undefined)
|
|
15
36
|
}
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
import type { InspectionHistoryRecord, InspectionHistoryResponse } from '@gscdump/contracts'
|
|
6
6
|
import type { ComputedRef, Ref } from '@vue/runtime-core'
|
|
7
7
|
import type { GscResourceStatus } from './_useGscResource'
|
|
8
|
+
import { gscQueries } from '../queries/gsc'
|
|
8
9
|
import { useGscResource } from './_useGscResource'
|
|
9
|
-
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
10
10
|
|
|
11
11
|
export interface UseGscInspectionHistoryReturn {
|
|
12
12
|
response: Readonly<Ref<InspectionHistoryResponse | null>>
|
|
13
13
|
records: ComputedRef<InspectionHistoryRecord[]>
|
|
14
14
|
url: ComputedRef<string | null>
|
|
15
15
|
loading: ComputedRef<boolean>
|
|
16
|
-
status: Ref<GscResourceStatus
|
|
17
|
-
error: Ref<Error | null
|
|
16
|
+
status: Readonly<Ref<GscResourceStatus>>
|
|
17
|
+
error: Readonly<Ref<Error | null>>
|
|
18
18
|
refresh: () => Promise<void>
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -22,9 +22,9 @@ export function useGscInspectionHistory(
|
|
|
22
22
|
siteId: MaybeRefOrGetter<string | null | undefined>,
|
|
23
23
|
urlHash: MaybeRefOrGetter<string | null | undefined>,
|
|
24
24
|
): UseGscInspectionHistoryReturn {
|
|
25
|
-
const { data, status, loading, error, refresh } = useGscResource({
|
|
25
|
+
const { data, status, loading, error, refresh } = useGscResource<[string, string], InspectionHistoryResponse>({
|
|
26
26
|
keys: [siteId, urlHash] as const,
|
|
27
|
-
|
|
27
|
+
operation: (id, hash) => gscQueries.inspectionHistory(id, hash),
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
const records = computed(() => data.value?.records ?? [])
|
|
@@ -5,23 +5,23 @@
|
|
|
5
5
|
import type { InspectionHistoryRecord, InspectionIndex } from '@gscdump/contracts'
|
|
6
6
|
import type { ComputedRef, Ref } from '@vue/runtime-core'
|
|
7
7
|
import type { GscResourceStatus } from './_useGscResource'
|
|
8
|
+
import { gscQueries } from '../queries/gsc'
|
|
8
9
|
import { useGscResource } from './_useGscResource'
|
|
9
|
-
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
10
10
|
|
|
11
11
|
export interface UseGscInspectionsReturn {
|
|
12
12
|
index: Readonly<Ref<InspectionIndex | null>>
|
|
13
13
|
records: ComputedRef<InspectionHistoryRecord[]>
|
|
14
14
|
statusCounts: ComputedRef<{ PASS: number, NEUTRAL: number, FAIL: number, unknown: number }>
|
|
15
15
|
loading: ComputedRef<boolean>
|
|
16
|
-
status: Ref<GscResourceStatus
|
|
17
|
-
error: Ref<Error | null
|
|
16
|
+
status: Readonly<Ref<GscResourceStatus>>
|
|
17
|
+
error: Readonly<Ref<Error | null>>
|
|
18
18
|
refresh: () => Promise<void>
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function useGscInspections(siteId: MaybeRefOrGetter<string | null | undefined>): UseGscInspectionsReturn {
|
|
22
|
-
const { data, status, loading, error, refresh } = useGscResource({
|
|
22
|
+
const { data, status, loading, error, refresh } = useGscResource<[string], InspectionIndex>({
|
|
23
23
|
keys: [siteId] as const,
|
|
24
|
-
|
|
24
|
+
operation: (id: string) => gscQueries.inspections(id),
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
const records = computed<InspectionHistoryRecord[]>(() =>
|
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
// `/analyze` provides these once; panels inject. Type-only — runtime values
|
|
7
7
|
// are the InjectionKey Symbols themselves.
|
|
8
8
|
|
|
9
|
+
import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
|
|
9
10
|
import type { InjectionKey } from '@vue/runtime-core'
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
export interface GscPanelRunner {
|
|
13
|
+
/** Positional-form SQL runner — `{ rows, queryMs }`. */
|
|
14
|
+
query: (sql: string, params?: readonly unknown[]) => Promise<{ rows: Record<string, unknown>[], queryMs: number }>
|
|
15
|
+
/** Registry-driven analyzer dispatch over the same per-site DuckDB-WASM runtime. */
|
|
16
|
+
analyze: (params: AnalysisParams, opts?: { signal?: AbortSignal }) => Promise<AnalysisResult & { queryMs: number }>
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
export interface GscPanelRunnerContext {
|
|
13
|
-
runner:
|
|
20
|
+
runner: GscPanelRunner
|
|
14
21
|
ready: Ref<boolean>
|
|
15
22
|
}
|
|
16
23
|
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
// AnalysisParams to `/api/__gsc/sites/[siteId]/analyze`. Override if your server
|
|
7
7
|
// uses a different contract.
|
|
8
8
|
|
|
9
|
-
import type { AnalysisParams, AnalysisResult } from '@gscdump/analysis'
|
|
9
|
+
import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
|
|
10
10
|
import type { ComputedRef, Ref, WatchSource } from '@vue/runtime-core'
|
|
11
|
+
import { gscQueries, withDefaultGscSearchType } from '../queries/gsc'
|
|
11
12
|
import { classifyGscError } from '../utils/gsc-error'
|
|
13
|
+
import { useGscRpc } from '../utils/gsc-rpc'
|
|
12
14
|
import { useGscBackfill } from './_useGscBackfill'
|
|
13
15
|
import { useGscQueryDispatcher } from './_useGscQueryDispatcher'
|
|
14
|
-
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
15
16
|
import { useGscAnalyzer } from './useGscAnalyzer'
|
|
16
17
|
import { _useGscAuthInternal } from './useGscAuth'
|
|
17
18
|
|
|
@@ -128,17 +129,14 @@ function isEmpty(v: unknown): boolean {
|
|
|
128
129
|
return false
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
async function defaultServerFallback<T>(siteId: string, params: AnalysisParams): Promise<T> {
|
|
132
|
-
return await useGscAnalyticsClient().analyze<T>(siteId, params)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
132
|
const DEFAULT_SEARCH_TYPE: NonNullable<AnalysisParams['searchType']> = 'web'
|
|
136
133
|
|
|
137
134
|
function withDefaultSearchType(params: AnalysisParams): AnalysisParams {
|
|
138
|
-
return
|
|
135
|
+
return withDefaultGscSearchType(params)
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
export function useGscQuery<T = AnalysisResult>(opts: UseGscQueryOptions<T>): UseGscQueryReturn<T> {
|
|
139
|
+
const rpc = useGscRpc()
|
|
142
140
|
const querySearchType = computed(() => toValue(opts.params).searchType ?? DEFAULT_SEARCH_TYPE)
|
|
143
141
|
const queryRange = computed(() => {
|
|
144
142
|
const params = toValue(opts.params)
|
|
@@ -176,7 +174,7 @@ export function useGscQuery<T = AnalysisResult>(opts: UseGscQueryOptions<T>): Us
|
|
|
176
174
|
|
|
177
175
|
async function runServer(siteId: string): Promise<void> {
|
|
178
176
|
const t0 = performance.now()
|
|
179
|
-
const fn = opts.serverFallback ??
|
|
177
|
+
const fn = opts.serverFallback ?? (async (siteId: string, params: AnalysisParams) => rpc.execute(gscQueries.analyze(siteId), params as never, { silent: true }) as Promise<T>)
|
|
180
178
|
const out = await fn(siteId, withDefaultSearchType(toValue(opts.params))) as T
|
|
181
179
|
data.value = out
|
|
182
180
|
engine.value = 'server'
|
|
@@ -1,122 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
mode: 'browser' | 'server'
|
|
16
|
-
reason: GscQueryDecisionReason
|
|
17
|
-
/** The resolved intent before the auth/optin mapping — useful for callers that branch on "did the user ask for auto?". */
|
|
18
|
-
requested: GscQueryEngine
|
|
19
|
-
detail?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface GscFallbackEvent {
|
|
23
|
-
reason: string
|
|
24
|
-
at: number
|
|
25
|
-
url: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface PickEngineOpts {
|
|
29
|
-
/** Per-call override. Wins over `useGscEngine()` + `runtimeConfig.public.analytics.defaultEngine`. */
|
|
30
|
-
perCall?: GscQueryEngine
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface GscQueryDispatcher {
|
|
34
|
-
/**
|
|
35
|
-
* Resolve the active engine into a concrete mode + reason. Consults the
|
|
36
|
-
* resolution chain: `opts.perCall` → `useGscEngine()` → runtimeConfig →
|
|
37
|
-
* `'auto'`. Then maps `auto` against `auth.browserAnalyzerEnabled`.
|
|
38
|
-
*/
|
|
39
|
-
pickEngine: (auth: InternalAuthState, opts?: PickEngineOpts) => GscEngineDecision
|
|
40
|
-
/** Called once per auto-fallback. Default impl is a no-op. */
|
|
41
|
-
reportFallback: (event: GscFallbackEvent) => void
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface CreateDefaultDispatcherOpts {
|
|
45
|
-
/** When set, fallbacks beacon to this URL with batched payloads. */
|
|
46
|
-
telemetryEndpoint?: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const FLUSH_DELAY_MS = 5000
|
|
50
|
-
|
|
51
|
-
function createReporter(endpoint: string): (event: GscFallbackEvent) => void {
|
|
52
|
-
const buffer: GscFallbackEvent[] = []
|
|
53
|
-
let flushScheduled = false
|
|
54
|
-
|
|
55
|
-
function flush(): void {
|
|
56
|
-
flushScheduled = false
|
|
57
|
-
if (buffer.length === 0)
|
|
58
|
-
return
|
|
59
|
-
const events = buffer.splice(0)
|
|
60
|
-
const body = JSON.stringify({ events })
|
|
61
|
-
if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
|
|
62
|
-
navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }))
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
fetch(endpoint, { method: 'POST', body, headers: { 'content-type': 'application/json' } }).catch(() => {})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function scheduleFlush(): void {
|
|
69
|
-
if (flushScheduled || typeof window === 'undefined')
|
|
70
|
-
return
|
|
71
|
-
flushScheduled = true
|
|
72
|
-
setTimeout(flush, FLUSH_DELAY_MS)
|
|
73
|
-
if (typeof document !== 'undefined') {
|
|
74
|
-
const handler = (): void => {
|
|
75
|
-
if (document.visibilityState === 'hidden')
|
|
76
|
-
flush()
|
|
77
|
-
}
|
|
78
|
-
document.addEventListener('visibilitychange', handler, { once: true })
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return (event: GscFallbackEvent): void => {
|
|
83
|
-
buffer.push(event)
|
|
84
|
-
scheduleFlush()
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function createDefaultGscQueryDispatcher(
|
|
89
|
-
opts: CreateDefaultDispatcherOpts = {},
|
|
90
|
-
): GscQueryDispatcher {
|
|
91
|
-
const reportFallback = opts.telemetryEndpoint
|
|
92
|
-
? createReporter(opts.telemetryEndpoint)
|
|
93
|
-
: (): void => {}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
pickEngine(auth, opts = {}): GscEngineDecision {
|
|
97
|
-
const requested = opts.perCall ?? resolveDefaultEngine()
|
|
98
|
-
if (requested === 'server')
|
|
99
|
-
return { mode: 'server', reason: 'forced:server', requested }
|
|
100
|
-
if (requested === 'browser')
|
|
101
|
-
return { mode: 'browser', reason: 'forced:browser', requested }
|
|
102
|
-
// 'auto': when the host has wired setGscAuth, derive from the per-user
|
|
103
|
-
// browserAnalyzerEnabled flag. When unwired, preserve legacy 'auto' = browser.
|
|
104
|
-
if (auth._initialized && !auth.browserAnalyzerEnabled)
|
|
105
|
-
return { mode: 'server', reason: 'optin:off', requested }
|
|
106
|
-
return { mode: 'browser', reason: 'auto:browser', requested }
|
|
107
|
-
},
|
|
108
|
-
reportFallback,
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function resolveDefaultEngine(): GscQueryEngine {
|
|
113
|
-
const override = useGscEngine().value
|
|
114
|
-
if (override)
|
|
115
|
-
return override
|
|
116
|
-
const cfg = useRuntimeConfig().public.analytics as { defaultEngine?: GscQueryEngine } | undefined
|
|
117
|
-
return cfg?.defaultEngine ?? 'auto'
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function useGscQueryDispatcher(): GscQueryDispatcher {
|
|
121
|
-
return useNuxtApp().$gscQueryDispatcher
|
|
122
|
-
}
|
|
1
|
+
// Public re-export. The implementation lives in `_useGscQueryDispatcher.ts`;
|
|
2
|
+
// the module's `imports:extend` hook strips `_use*` from host auto-imports so
|
|
3
|
+
// consumers see the wrapper at `useGscQueryDispatcher` and the layer's own
|
|
4
|
+
// composables import the internal file directly. Named re-exports because
|
|
5
|
+
// Nuxt's auto-import scanner doesn't traverse `export *`.
|
|
6
|
+
export {
|
|
7
|
+
type CreateDefaultDispatcherOpts,
|
|
8
|
+
createDefaultGscQueryDispatcher,
|
|
9
|
+
type GscEngineDecision,
|
|
10
|
+
type GscFallbackEvent,
|
|
11
|
+
type GscQueryDispatcher,
|
|
12
|
+
type PickEngineOpts,
|
|
13
|
+
useGscQueryDispatcher,
|
|
14
|
+
} from './_useGscQueryDispatcher'
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import type { IndexingInspectRateLimited, IndexingInspectResponse } from '@gscdump/contracts'
|
|
2
|
-
import {
|
|
2
|
+
import { invalidateNuxtQueries } from 'nuxt-use-query/query-cache'
|
|
3
|
+
import { gscQueries } from '../queries/gsc'
|
|
4
|
+
import { useGscRpc } from '../utils/gsc-rpc'
|
|
3
5
|
|
|
4
6
|
export function useGscRequestIndexingInspect(): (
|
|
5
7
|
siteId: string,
|
|
6
8
|
urls: string[],
|
|
7
9
|
) => Promise<IndexingInspectResponse | IndexingInspectRateLimited> {
|
|
8
|
-
const
|
|
10
|
+
const rpc = useGscRpc()
|
|
9
11
|
return async (siteId, urls) => {
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
return rpc.execute(gscQueries.indexingInspect(siteId), { urls }, { silent: true })
|
|
13
|
+
.then((res) => {
|
|
14
|
+
invalidateNuxtQueries(`gsc:inspections:${siteId}`)
|
|
15
|
+
invalidateNuxtQueries(`gsc:inspection-history:${siteId}`)
|
|
16
|
+
return res as IndexingInspectResponse | IndexingInspectRateLimited
|
|
17
|
+
})
|
|
18
|
+
.catch((err: unknown) => {
|
|
19
|
+
const e = err as { type?: string, status?: number, statusCode?: number, response?: { status?: number }, data?: unknown }
|
|
20
|
+
const status = e.status
|
|
21
|
+
?? e.statusCode
|
|
22
|
+
?? e.response?.status
|
|
23
|
+
const data = e.data as IndexingInspectRateLimited | undefined
|
|
24
|
+
if (status === 429 && data && data.error === 'rate_limited')
|
|
25
|
+
return data
|
|
26
|
+
throw err
|
|
27
|
+
})
|
|
19
28
|
}
|
|
20
29
|
}
|