@gscdump/nuxt 0.22.4 → 1.5.1
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 +10 -137
- package/dist/module.d.mts +22 -0
- package/dist/module.mjs +30 -0
- package/dist/runtime-config.d.mts +12 -0
- package/dist/runtime-config.mjs +27 -0
- package/package.json +26 -176
- package/app/assets/css/main.css +0 -2
- package/app/components/GscAnalyzerPanel.vue +0 -94
- package/app/components/GscAnonymizationBanner.vue +0 -46
- package/app/components/GscBootProgress.vue +0 -297
- package/app/components/GscCommandPalette.vue +0 -77
- package/app/components/GscDashboardPage.vue +0 -26
- package/app/components/GscEngineBadge.vue +0 -28
- package/app/components/GscHero.vue +0 -214
- package/app/components/GscLazyTopResult.vue +0 -47
- package/app/components/GscPerformanceChart.vue +0 -532
- package/app/components/GscPositionDistributionChart.vue +0 -63
- package/app/components/GscQueryLabel.vue +0 -63
- package/app/components/GscSitePageHeader.vue +0 -40
- package/app/components/GscSourceDebugPanel.vue +0 -196
- package/app/components/GscSyncStatusCell.vue +0 -54
- package/app/components/GscTopRollupCard.vue +0 -90
- package/app/composables/_useGscBackfill.ts +0 -119
- package/app/composables/_useGscQueryDispatcher.ts +0 -123
- package/app/composables/_useGscResource.ts +0 -202
- package/app/composables/_useGscSharedSiteResource.ts +0 -136
- package/app/composables/useGscAnalytics.ts +0 -206
- package/app/composables/useGscAnalyticsClient.ts +0 -9
- package/app/composables/useGscAnalyticsConfig.ts +0 -8
- package/app/composables/useGscAnalyticsSourceInfo.ts +0 -165
- package/app/composables/useGscAnalyzer.ts +0 -460
- package/app/composables/useGscAnalyzerBatch.ts +0 -106
- package/app/composables/useGscAnalyzerDefs.ts +0 -119
- package/app/composables/useGscAuth.ts +0 -115
- package/app/composables/useGscConsoleUrl.ts +0 -45
- package/app/composables/useGscCountries.ts +0 -46
- package/app/composables/useGscCurrentSite.ts +0 -36
- package/app/composables/useGscInspectionHistory.ts +0 -42
- package/app/composables/useGscInspections.ts +0 -52
- package/app/composables/useGscPanelContext.ts +0 -31
- package/app/composables/useGscPeriod.ts +0 -243
- package/app/composables/useGscQuery.ts +0 -301
- package/app/composables/useGscQueryDispatcher.ts +0 -14
- package/app/composables/useGscRequestIndexingInspect.ts +0 -29
- package/app/composables/useGscResource.ts +0 -11
- package/app/composables/useGscRollup.ts +0 -237
- package/app/composables/useGscSearchAppearance.ts +0 -46
- package/app/composables/useGscSiteAnalyzer.ts +0 -456
- package/app/composables/useGscSitemapHistory.ts +0 -41
- package/app/composables/useGscSitemaps.ts +0 -45
- package/app/composables/useGscTableState.ts +0 -119
- package/app/plugins/analytics.ts +0 -57
- package/app/queries/gsc.ts +0 -163
- package/app/utils/anonymization.ts +0 -24
- package/app/utils/country-names.ts +0 -56
- package/app/utils/duckdb-wasm.ts +0 -166
- package/app/utils/gsc-constants.ts +0 -10
- package/app/utils/gsc-error.ts +0 -58
- package/app/utils/gsc-fetch.ts +0 -94
- package/app/utils/gsc-filters.ts +0 -32
- package/app/utils/gsc-rows.ts +0 -72
- package/app/utils/gsc-rpc.ts +0 -18
- package/app/utils/position.ts +0 -7
- package/module.ts +0 -81
- package/nuxt.config.ts +0 -61
- package/types.ts +0 -115
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
// Period + comparison primitives. Pure utilities (date math, growth calc)
|
|
2
|
-
// plus the reactive `useGscPeriod()` composable that wires them into refs.
|
|
3
|
-
//
|
|
4
|
-
// Supports rolling presets (7d/28d/3m/6m/12m), calendar presets (this-week,
|
|
5
|
-
// this-month, last-month, this-quarter, this-year), and custom ranges
|
|
6
|
-
// (`custom:start:end` or `custom:start:end:prevStart:prevEnd`).
|
|
7
|
-
//
|
|
8
|
-
// Timezone via `runtimeConfig.public.analytics.timezone` (IANA name) or
|
|
9
|
-
// the `timezone` option. Default: UTC.
|
|
10
|
-
|
|
11
|
-
import { GSC_STABLE_LATENCY_DAYS } from '../utils/gsc-constants'
|
|
12
|
-
|
|
13
|
-
export type RollingPeriod = '7d' | '28d' | '3m' | '6m' | '12m'
|
|
14
|
-
export type CalendarPeriod = 'this-week' | 'this-month' | 'last-month' | 'this-quarter' | 'this-year'
|
|
15
|
-
export type CustomPeriod = `custom:${string}:${string}` | `custom:${string}:${string}:${string}:${string}`
|
|
16
|
-
export type Period = RollingPeriod | CalendarPeriod | CustomPeriod
|
|
17
|
-
export type CompareMode = 'previous' | 'year' | 'none'
|
|
18
|
-
|
|
19
|
-
export interface PeriodPreset {
|
|
20
|
-
value: RollingPeriod | CalendarPeriod
|
|
21
|
-
label: string
|
|
22
|
-
shortLabel: string
|
|
23
|
-
days: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const PERIOD_PRESETS = [
|
|
27
|
-
{ value: '7d', label: 'Last 7 days', shortLabel: '7d', days: 7 },
|
|
28
|
-
{ value: '28d', label: 'Last 28 days', shortLabel: '28d', days: 28 },
|
|
29
|
-
{ value: '3m', label: 'Last 3 months', shortLabel: '3m', days: 90 },
|
|
30
|
-
{ value: '6m', label: 'Last 6 months', shortLabel: '6m', days: 180 },
|
|
31
|
-
{ value: '12m', label: 'Last 12 months', shortLabel: '12m', days: 365 },
|
|
32
|
-
{ value: 'this-week', label: 'This week', shortLabel: 'WTD', days: 7 },
|
|
33
|
-
{ value: 'this-month', label: 'This month', shortLabel: 'MTD', days: 31 },
|
|
34
|
-
{ value: 'last-month', label: 'Last month', shortLabel: 'LM', days: 31 },
|
|
35
|
-
{ value: 'this-quarter', label: 'This quarter', shortLabel: 'QTD', days: 92 },
|
|
36
|
-
{ value: 'this-year', label: 'This year', shortLabel: 'YTD', days: 365 },
|
|
37
|
-
] as const satisfies readonly PeriodPreset[]
|
|
38
|
-
|
|
39
|
-
export const COMPARE_OPTIONS: readonly { value: CompareMode, label: string }[] = [
|
|
40
|
-
{ value: 'previous', label: 'Previous period' },
|
|
41
|
-
{ value: 'year', label: 'Year over year' },
|
|
42
|
-
{ value: 'none', label: 'No comparison' },
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
export interface DateRange {
|
|
46
|
-
start: string
|
|
47
|
-
end: string
|
|
48
|
-
days: number
|
|
49
|
-
prevStart: string
|
|
50
|
-
prevEnd: string
|
|
51
|
-
yearStart: string
|
|
52
|
-
yearEnd: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function isCustomPeriod(p: Period | string): p is CustomPeriod {
|
|
56
|
-
return typeof p === 'string' && p.startsWith('custom:')
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function parseCustomPeriod(p: Period | string): { start: string, end: string, prevStart?: string, prevEnd?: string } | null {
|
|
60
|
-
if (!isCustomPeriod(p))
|
|
61
|
-
return null
|
|
62
|
-
const [, start, end, prevStart, prevEnd] = p.split(':')
|
|
63
|
-
if (!start || !end)
|
|
64
|
-
return null
|
|
65
|
-
return prevStart && prevEnd ? { start, end, prevStart, prevEnd } : { start, end }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface PeriodOptions {
|
|
69
|
-
/** Subtract GSC's stable-data latency from `end`. Default `true`. */
|
|
70
|
-
stableData?: boolean
|
|
71
|
-
/** IANA timezone (e.g. 'America/Los_Angeles'). Default UTC. */
|
|
72
|
-
timezone?: string
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function periodToDateRange(period: Period | string, opts: PeriodOptions = {}): DateRange {
|
|
76
|
-
const stableData = opts.stableData ?? true
|
|
77
|
-
const custom = parseCustomPeriod(period)
|
|
78
|
-
if (custom) {
|
|
79
|
-
const range = buildRange(parseIso(custom.start), parseIso(custom.end))
|
|
80
|
-
if (custom.prevStart && custom.prevEnd) {
|
|
81
|
-
return {
|
|
82
|
-
...range,
|
|
83
|
-
prevStart: custom.prevStart,
|
|
84
|
-
prevEnd: custom.prevEnd,
|
|
85
|
-
yearStart: custom.prevStart,
|
|
86
|
-
yearEnd: custom.prevEnd,
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return range
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const today = todayInTz(opts.timezone)
|
|
93
|
-
const end = stableData ? addDays(today, -GSC_STABLE_LATENCY_DAYS) : addDays(today, -1)
|
|
94
|
-
|
|
95
|
-
switch (period) {
|
|
96
|
-
case '7d': return buildRange(addDays(end, -6), end)
|
|
97
|
-
case '28d': return buildRange(addDays(end, -27), end)
|
|
98
|
-
case '3m': return buildRange(addDays(end, -89), end)
|
|
99
|
-
case '6m': return buildRange(addDays(end, -179), end)
|
|
100
|
-
case '12m': return buildRange(addDays(end, -364), end)
|
|
101
|
-
case 'this-week': return buildRange(startOfWeek(end), end)
|
|
102
|
-
case 'this-month': return buildRange(startOfMonth(end), end)
|
|
103
|
-
case 'last-month': {
|
|
104
|
-
const prev = addDays(startOfMonth(end), -1)
|
|
105
|
-
return buildRange(startOfMonth(prev), endOfMonth(prev))
|
|
106
|
-
}
|
|
107
|
-
case 'this-quarter': return buildRange(startOfQuarter(end), end)
|
|
108
|
-
case 'this-year': return buildRange(startOfYear(end), end)
|
|
109
|
-
default: return buildRange(addDays(end, -27), end)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function periodToDays(period: Period | string, opts?: PeriodOptions): number {
|
|
114
|
-
return periodToDateRange(period, opts).days
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function compareRange(range: DateRange, mode: CompareMode): { start: string, end: string } | null {
|
|
118
|
-
if (mode === 'none')
|
|
119
|
-
return null
|
|
120
|
-
if (mode === 'year')
|
|
121
|
-
return { start: range.yearStart, end: range.yearEnd }
|
|
122
|
-
return { start: range.prevStart, end: range.prevEnd }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function getPeriodLabel(period: Period | string): string {
|
|
126
|
-
if (isCustomPeriod(period)) {
|
|
127
|
-
const c = parseCustomPeriod(period)
|
|
128
|
-
return c ? `${c.start} → ${c.end}` : 'Custom'
|
|
129
|
-
}
|
|
130
|
-
return PERIOD_PRESETS.find(p => p.value === period)?.label ?? String(period)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Relative growth as a fraction (0.12 = +12%). `null` if prev is zero or
|
|
135
|
-
* either side missing — caller renders "—" rather than divide-by-zero.
|
|
136
|
-
*/
|
|
137
|
-
export function computeGrowth(current: number, previous: number | null | undefined): number | null {
|
|
138
|
-
if (previous == null || previous === 0)
|
|
139
|
-
return null
|
|
140
|
-
return (current - previous) / previous
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export interface UseGscPeriodOptions {
|
|
144
|
-
defaultPeriod?: Period
|
|
145
|
-
defaultCompareMode?: CompareMode
|
|
146
|
-
defaultStableData?: boolean
|
|
147
|
-
/** Override `runtimeConfig.public.analytics.timezone`. */
|
|
148
|
-
timezone?: string
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface UseGscPeriodReturn {
|
|
152
|
-
period: Ref<Period>
|
|
153
|
-
compareMode: Ref<CompareMode>
|
|
154
|
-
stableData: Ref<boolean>
|
|
155
|
-
range: ComputedRef<DateRange>
|
|
156
|
-
comparison: ComputedRef<{ start: string, end: string } | null>
|
|
157
|
-
label: ComputedRef<string>
|
|
158
|
-
presets: typeof PERIOD_PRESETS
|
|
159
|
-
compareOptions: typeof COMPARE_OPTIONS
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function useGscPeriod(opts: UseGscPeriodOptions = {}): UseGscPeriodReturn {
|
|
163
|
-
const period = useState<Period>('gsc:period', () => opts.defaultPeriod ?? '28d')
|
|
164
|
-
const compareMode = useState<CompareMode>('gsc:compareMode', () => opts.defaultCompareMode ?? 'previous')
|
|
165
|
-
const stableData = useState<boolean>('gsc:stableData', () => opts.defaultStableData ?? true)
|
|
166
|
-
|
|
167
|
-
const cfg = useRuntimeConfig().public.analytics as { timezone?: string } | undefined
|
|
168
|
-
const timezone = opts.timezone ?? cfg?.timezone ?? undefined
|
|
169
|
-
|
|
170
|
-
const range = computed(() => periodToDateRange(period.value, { stableData: stableData.value, timezone }))
|
|
171
|
-
const comparison = computed(() => compareRange(range.value, compareMode.value))
|
|
172
|
-
const label = computed(() => getPeriodLabel(period.value))
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
period,
|
|
176
|
-
compareMode,
|
|
177
|
-
stableData,
|
|
178
|
-
range,
|
|
179
|
-
comparison,
|
|
180
|
-
label,
|
|
181
|
-
presets: PERIOD_PRESETS,
|
|
182
|
-
compareOptions: COMPARE_OPTIONS,
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function todayInTz(tz?: string): Date {
|
|
187
|
-
if (!tz) {
|
|
188
|
-
const now = new Date()
|
|
189
|
-
return new Date(`${now.toISOString().slice(0, 10)}T00:00:00Z`)
|
|
190
|
-
}
|
|
191
|
-
const fmt = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' })
|
|
192
|
-
return new Date(`${fmt.format(new Date())}T00:00:00Z`)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function parseIso(s: string): Date {
|
|
196
|
-
return new Date(`${s}T00:00:00Z`)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function addDays(d: Date, n: number): Date {
|
|
200
|
-
const out = new Date(d)
|
|
201
|
-
out.setUTCDate(out.getUTCDate() + n)
|
|
202
|
-
return out
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function iso(d: Date): string {
|
|
206
|
-
return d.toISOString().slice(0, 10)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function startOfWeek(d: Date): Date {
|
|
210
|
-
const day = d.getUTCDay()
|
|
211
|
-
return addDays(d, day === 0 ? -6 : 1 - day)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function startOfMonth(d: Date): Date {
|
|
215
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1))
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function endOfMonth(d: Date): Date {
|
|
219
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0))
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function startOfQuarter(d: Date): Date {
|
|
223
|
-
return new Date(Date.UTC(d.getUTCFullYear(), Math.floor(d.getUTCMonth() / 3) * 3, 1))
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function startOfYear(d: Date): Date {
|
|
227
|
-
return new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function buildRange(start: Date, end: Date): DateRange {
|
|
231
|
-
const days = Math.round((end.getTime() - start.getTime()) / 86400000) + 1
|
|
232
|
-
const prevEnd = addDays(start, -1)
|
|
233
|
-
const prevStart = addDays(prevEnd, -(days - 1))
|
|
234
|
-
return {
|
|
235
|
-
start: iso(start),
|
|
236
|
-
end: iso(end),
|
|
237
|
-
days,
|
|
238
|
-
prevStart: iso(prevStart),
|
|
239
|
-
prevEnd: iso(prevEnd),
|
|
240
|
-
yearStart: iso(addDays(start, -365)),
|
|
241
|
-
yearEnd: iso(addDays(end, -365)),
|
|
242
|
-
}
|
|
243
|
-
}
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
// Unified analytics query hook. Dispatches browser DuckDB-WASM vs server
|
|
2
|
-
// fallback, abort-safe, with typed status enum and opt-in backfill-on-demand.
|
|
3
|
-
//
|
|
4
|
-
// `site` is a parameter (not global state) so multi-site pages can issue
|
|
5
|
-
// independent queries. `serverFallback` is optional — the default POSTs
|
|
6
|
-
// AnalysisParams to `/api/__gsc/sites/[siteId]/analyze`. Override if your server
|
|
7
|
-
// uses a different contract.
|
|
8
|
-
|
|
9
|
-
import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
|
|
10
|
-
import type { ComputedRef, Ref, WatchSource } from '@vue/runtime-core'
|
|
11
|
-
import { gscQueries, withDefaultGscSearchType } from '../queries/gsc'
|
|
12
|
-
import { classifyGscError } from '../utils/gsc-error'
|
|
13
|
-
import { useGscRpc } from '../utils/gsc-rpc'
|
|
14
|
-
import { useGscBackfill } from './_useGscBackfill'
|
|
15
|
-
import { useGscQueryDispatcher } from './_useGscQueryDispatcher'
|
|
16
|
-
import { useGscAnalyzer } from './useGscAnalyzer'
|
|
17
|
-
import { _useGscAuthInternal } from './useGscAuth'
|
|
18
|
-
|
|
19
|
-
export type GscQueryEngine = 'auto' | 'browser' | 'server'
|
|
20
|
-
|
|
21
|
-
export type GscQueryStatus
|
|
22
|
-
= | 'idle'
|
|
23
|
-
| 'pending'
|
|
24
|
-
| 'success'
|
|
25
|
-
| 'empty'
|
|
26
|
-
| 'error'
|
|
27
|
-
| 'auth-missing'
|
|
28
|
-
| 'rate-limited'
|
|
29
|
-
| 'network'
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Why a given query ended up on `browser` or `server`. Surfaced for
|
|
33
|
-
* dev tooling so 0% R2 utilisation can be diagnosed without guessing
|
|
34
|
-
* (e.g. opt-in off vs. site not eligible vs. attach failure).
|
|
35
|
-
*/
|
|
36
|
-
export type GscQueryDecisionReason
|
|
37
|
-
= | 'idle'
|
|
38
|
-
| 'ssr'
|
|
39
|
-
| 'disabled'
|
|
40
|
-
| 'forced:server'
|
|
41
|
-
| 'forced:browser'
|
|
42
|
-
| 'optin:off'
|
|
43
|
-
| 'auto:browser'
|
|
44
|
-
| 'auto:fallback'
|
|
45
|
-
|
|
46
|
-
export interface GscQueryDecision {
|
|
47
|
-
mode: 'browser' | 'server' | null
|
|
48
|
-
reason: GscQueryDecisionReason
|
|
49
|
-
/** Free-text detail for `auto:fallback` (the underlying error message). */
|
|
50
|
-
detail?: string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface GscQueryMeta {
|
|
54
|
-
raw: Record<string, unknown> | null
|
|
55
|
-
backfillRequired?: { startDate: string, endDate: string }
|
|
56
|
-
retryAfter?: number
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export type GscBackfillRunner = ReturnType<typeof useGscBackfill>
|
|
60
|
-
|
|
61
|
-
export interface UseGscQueryOptions<T> {
|
|
62
|
-
/** Site id (reactive). Required — queries don't dispatch without a site. */
|
|
63
|
-
site: MaybeRefOrGetter<string | null | undefined>
|
|
64
|
-
/** Reactive analysis params (discriminated union by `.type`). */
|
|
65
|
-
params: MaybeRefOrGetter<AnalysisParams>
|
|
66
|
-
/**
|
|
67
|
-
* Transform raw `AnalysisResult` from the browser path into the consumer type.
|
|
68
|
-
* Omit to pass through as-is (return type becomes `AnalysisResult`).
|
|
69
|
-
*/
|
|
70
|
-
reshape?: (raw: AnalysisResult) => T
|
|
71
|
-
/**
|
|
72
|
-
* Server fallback override. Defaults to POSTing the params to
|
|
73
|
-
* `/api/__gsc/sites/[siteId]/analyze` and returning the response body.
|
|
74
|
-
*/
|
|
75
|
-
serverFallback?: (siteId: string, params: AnalysisParams) => Promise<T>
|
|
76
|
-
/**
|
|
77
|
-
* Force a specific engine. Default `'auto'`. Accepts a getter so callers
|
|
78
|
-
* can swing engine reactively (e.g. flip to `'server'` when the requested
|
|
79
|
-
* range overlaps a known coverage gap). Re-read on every `runQuery` and
|
|
80
|
-
* also included in the watcher graph so changes trigger a refetch.
|
|
81
|
-
*/
|
|
82
|
-
engine?: MaybeRefOrGetter<GscQueryEngine>
|
|
83
|
-
/** Extra reactive sources that should trigger refetch. */
|
|
84
|
-
watchSources?: WatchSource[]
|
|
85
|
-
/** Extract meta from the consumer payload. Defaults to `(out as any).meta`. */
|
|
86
|
-
extractMeta?: (out: T) => Record<string, unknown> | null | undefined
|
|
87
|
-
/**
|
|
88
|
-
* Backfill handling. `false` (default) = off. `true` = spin up a dedicated
|
|
89
|
-
* `useGscBackfill()`. Pass an existing runner to share across queries.
|
|
90
|
-
*/
|
|
91
|
-
backfill?: boolean | GscBackfillRunner
|
|
92
|
-
/** Gate the query — when `false` it stays idle. */
|
|
93
|
-
enabled?: MaybeRefOrGetter<boolean>
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface UseGscQueryReturn<T> {
|
|
97
|
-
data: Ref<T | null>
|
|
98
|
-
status: Ref<GscQueryStatus>
|
|
99
|
-
pending: ComputedRef<boolean>
|
|
100
|
-
error: Ref<Error | null>
|
|
101
|
-
engine: Ref<'browser' | 'server' | null>
|
|
102
|
-
elapsedMs: Ref<number | null>
|
|
103
|
-
fallbackReason: Ref<string | null>
|
|
104
|
-
/** Why this query ran where it ran. Updated on every dispatch. */
|
|
105
|
-
lastDecision: Ref<GscQueryDecision>
|
|
106
|
-
meta: Ref<GscQueryMeta>
|
|
107
|
-
backfill: GscBackfillRunner | null
|
|
108
|
-
refresh: () => Promise<void>
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function classifyError(e: unknown): { status: GscQueryStatus, retryAfter?: number } {
|
|
112
|
-
const c = classifyGscError(e)
|
|
113
|
-
return { status: c.status as GscQueryStatus, retryAfter: c.retryAfter }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isEmpty(v: unknown): boolean {
|
|
117
|
-
if (v == null)
|
|
118
|
-
return true
|
|
119
|
-
if (Array.isArray(v))
|
|
120
|
-
return v.length === 0
|
|
121
|
-
if (typeof v === 'object') {
|
|
122
|
-
const rec = v as Record<string, unknown>
|
|
123
|
-
for (const key of ['rows', 'results', 'daily', 'items']) {
|
|
124
|
-
const arr = rec[key]
|
|
125
|
-
if (Array.isArray(arr))
|
|
126
|
-
return arr.length === 0
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return false
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const DEFAULT_SEARCH_TYPE: NonNullable<AnalysisParams['searchType']> = 'web'
|
|
133
|
-
|
|
134
|
-
function withDefaultSearchType(params: AnalysisParams): AnalysisParams {
|
|
135
|
-
return withDefaultGscSearchType(params)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function useGscQuery<T = AnalysisResult>(opts: UseGscQueryOptions<T>): UseGscQueryReturn<T> {
|
|
139
|
-
const rpc = useGscRpc()
|
|
140
|
-
const querySearchType = computed(() => toValue(opts.params).searchType ?? DEFAULT_SEARCH_TYPE)
|
|
141
|
-
const queryRange = computed(() => {
|
|
142
|
-
const params = toValue(opts.params)
|
|
143
|
-
return typeof params.startDate === 'string' && typeof params.endDate === 'string'
|
|
144
|
-
? { start: params.startDate, end: params.endDate }
|
|
145
|
-
: null
|
|
146
|
-
})
|
|
147
|
-
const analyzer = useGscAnalyzer(opts.site, querySearchType, queryRange)
|
|
148
|
-
const dispatcher = useGscQueryDispatcher()
|
|
149
|
-
|
|
150
|
-
const data = shallowRef<T | null>(null)
|
|
151
|
-
const status = ref<GscQueryStatus>('idle')
|
|
152
|
-
const error = ref<Error | null>(null)
|
|
153
|
-
const engine = ref<'browser' | 'server' | null>(null)
|
|
154
|
-
const elapsedMs = ref<number | null>(null)
|
|
155
|
-
const fallbackReason = ref<string | null>(null)
|
|
156
|
-
const lastDecision = ref<GscQueryDecision>({ mode: null, reason: 'idle' })
|
|
157
|
-
const meta = ref<GscQueryMeta>({ raw: null })
|
|
158
|
-
const pending = computed(() => status.value === 'pending')
|
|
159
|
-
|
|
160
|
-
function extractMeta(out: T): Record<string, unknown> | null {
|
|
161
|
-
if (opts.extractMeta) {
|
|
162
|
-
const m = opts.extractMeta(out)
|
|
163
|
-
return m ?? null
|
|
164
|
-
}
|
|
165
|
-
const m = (out as unknown as { meta?: Record<string, unknown> })?.meta
|
|
166
|
-
return m ?? null
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function captureMeta(out: T): void {
|
|
170
|
-
const raw = extractMeta(out)
|
|
171
|
-
const backfillRequired = (raw as { backfillRequired?: { startDate: string, endDate: string } } | null)?.backfillRequired
|
|
172
|
-
meta.value = backfillRequired ? { raw, backfillRequired } : { raw }
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function runServer(siteId: string): Promise<void> {
|
|
176
|
-
const t0 = performance.now()
|
|
177
|
-
const fn = opts.serverFallback ?? (async (siteId: string, params: AnalysisParams) => rpc.execute(gscQueries.analyze(siteId), params as never, { silent: true }) as Promise<T>)
|
|
178
|
-
const out = await fn(siteId, withDefaultSearchType(toValue(opts.params))) as T
|
|
179
|
-
data.value = out
|
|
180
|
-
engine.value = 'server'
|
|
181
|
-
elapsedMs.value = performance.now() - t0
|
|
182
|
-
captureMeta(out)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function runBrowser(signal: AbortSignal): Promise<void> {
|
|
186
|
-
const out = await analyzer.analyze(withDefaultSearchType(toValue(opts.params)), { signal })
|
|
187
|
-
signal.throwIfAborted()
|
|
188
|
-
const shaped = opts.reshape ? opts.reshape(out) : (out as unknown as T)
|
|
189
|
-
data.value = shaped
|
|
190
|
-
engine.value = 'browser'
|
|
191
|
-
elapsedMs.value = (out as unknown as { queryMs?: number }).queryMs ?? null
|
|
192
|
-
captureMeta(shaped)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
let activeController: AbortController | null = null
|
|
196
|
-
|
|
197
|
-
async function runQuery(): Promise<void> {
|
|
198
|
-
if (!import.meta.client) {
|
|
199
|
-
status.value = 'idle'
|
|
200
|
-
lastDecision.value = { mode: null, reason: 'ssr' }
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
if (opts.enabled && !toValue(opts.enabled)) {
|
|
204
|
-
status.value = 'idle'
|
|
205
|
-
data.value = null
|
|
206
|
-
engine.value = null
|
|
207
|
-
elapsedMs.value = null
|
|
208
|
-
lastDecision.value = { mode: null, reason: 'disabled' }
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
const siteId = toValue(opts.site)
|
|
212
|
-
if (!siteId) {
|
|
213
|
-
status.value = 'idle'
|
|
214
|
-
lastDecision.value = { mode: null, reason: 'idle' }
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
activeController?.abort()
|
|
218
|
-
const controller = new AbortController()
|
|
219
|
-
activeController = controller
|
|
220
|
-
status.value = 'pending'
|
|
221
|
-
error.value = null
|
|
222
|
-
fallbackReason.value = null
|
|
223
|
-
|
|
224
|
-
const decision = dispatcher.pickEngine(_useGscAuthInternal().value, { perCall: toValue(opts.engine) })
|
|
225
|
-
lastDecision.value = decision
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
if (decision.mode === 'server') {
|
|
229
|
-
await runServer(siteId)
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
// Browser-mode: when the caller asked for `auto` we fall back to server
|
|
233
|
-
// on failure; explicit `browser` propagates the error.
|
|
234
|
-
await runBrowser(controller.signal).catch(async (e) => {
|
|
235
|
-
if (e?.name === 'AbortError')
|
|
236
|
-
throw e
|
|
237
|
-
if (decision.requested !== 'auto')
|
|
238
|
-
throw e
|
|
239
|
-
fallbackReason.value = e instanceof Error ? e.message : String(e)
|
|
240
|
-
console.warn('[useGscQuery] browser failed, falling back to server:', fallbackReason.value)
|
|
241
|
-
dispatcher.reportFallback({
|
|
242
|
-
reason: fallbackReason.value,
|
|
243
|
-
at: Date.now(),
|
|
244
|
-
url: typeof location !== 'undefined' ? location.pathname : '',
|
|
245
|
-
})
|
|
246
|
-
lastDecision.value = { mode: 'server', reason: 'auto:fallback', detail: fallbackReason.value }
|
|
247
|
-
await runServer(siteId)
|
|
248
|
-
})
|
|
249
|
-
}
|
|
250
|
-
if (!controller.signal.aborted)
|
|
251
|
-
status.value = isEmpty(data.value) ? 'empty' : 'success'
|
|
252
|
-
}
|
|
253
|
-
catch (e) {
|
|
254
|
-
if ((e as { name?: string })?.name === 'AbortError')
|
|
255
|
-
return
|
|
256
|
-
error.value = e instanceof Error ? e : new Error(String(e))
|
|
257
|
-
const classified = classifyError(e)
|
|
258
|
-
status.value = classified.status
|
|
259
|
-
if (classified.retryAfter != null)
|
|
260
|
-
meta.value = { ...meta.value, retryAfter: classified.retryAfter }
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
if (activeController === controller)
|
|
264
|
-
activeController = null
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (import.meta.client) {
|
|
269
|
-
onScopeDispose(() => {
|
|
270
|
-
activeController?.abort()
|
|
271
|
-
activeController = null
|
|
272
|
-
})
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const sources: WatchSource[] = [
|
|
276
|
-
() => toValue(opts.site),
|
|
277
|
-
() => toValue(opts.params),
|
|
278
|
-
...(opts.watchSources ?? []),
|
|
279
|
-
]
|
|
280
|
-
if (opts.enabled)
|
|
281
|
-
sources.push(() => toValue(opts.enabled))
|
|
282
|
-
if (opts.engine !== undefined)
|
|
283
|
-
sources.push(() => toValue(opts.engine))
|
|
284
|
-
watch(sources, runQuery, { deep: true, immediate: true })
|
|
285
|
-
|
|
286
|
-
let backfill: GscBackfillRunner | null = null
|
|
287
|
-
const b = opts.backfill ?? false
|
|
288
|
-
if (b !== false) {
|
|
289
|
-
backfill = b === true ? useGscBackfill() : b
|
|
290
|
-
watch(() => meta.value.backfillRequired, (req: { startDate: string, endDate: string } | undefined) => {
|
|
291
|
-
if (!req)
|
|
292
|
-
return
|
|
293
|
-
const siteId = toValue(opts.site)
|
|
294
|
-
if (!siteId)
|
|
295
|
-
return
|
|
296
|
-
backfill!.maybeTrigger({ backfillRequired: req }, siteId, runQuery)
|
|
297
|
-
}, { immediate: true })
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return { data, status, pending, error, engine, elapsedMs, fallbackReason, lastDecision, meta, backfill, refresh: runQuery }
|
|
301
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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,29 +0,0 @@
|
|
|
1
|
-
import type { IndexingInspectRateLimited, IndexingInspectResponse } from '@gscdump/contracts'
|
|
2
|
-
import { invalidateNuxtQueries } from 'nuxt-use-query/query-cache'
|
|
3
|
-
import { gscQueries } from '../queries/gsc'
|
|
4
|
-
import { useGscRpc } from '../utils/gsc-rpc'
|
|
5
|
-
|
|
6
|
-
export function useGscRequestIndexingInspect(): (
|
|
7
|
-
siteId: string,
|
|
8
|
-
urls: string[],
|
|
9
|
-
) => Promise<IndexingInspectResponse | IndexingInspectRateLimited> {
|
|
10
|
-
const rpc = useGscRpc()
|
|
11
|
-
return async (siteId, urls) => {
|
|
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
|
-
})
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Public re-export. The implementation lives in `_useGscResource.ts`; the
|
|
2
|
-
// module's `imports:extend` hook strips `_use*` from host auto-imports so
|
|
3
|
-
// consumers see the wrapper at `useGscResource` 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 GscResourceStatus,
|
|
8
|
-
useGscResource,
|
|
9
|
-
type UseGscResourceOptions,
|
|
10
|
-
type UseGscResourceReturn,
|
|
11
|
-
} from './_useGscResource'
|