@gscdump/devframe 5.7.0 → 5.8.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/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/account.mjs +2 -0
- package/dist/client/assets/{browser-agent-rpc-BXhoSh1z-CQwaKeCZ.js → browser-agent-rpc-BXhoSh1z-UuY7Hinl.js} +1 -1
- package/dist/client/assets/index-BdO8MoYt.css +1 -0
- package/dist/client/assets/{index-B8m88k2l.js → index-DA7Lgn7_.js} +3 -3
- package/dist/client/index.html +2 -2
- package/dist/index.d.mts +5 -3
- package/dist/index.mjs +5 -4
- package/dist/package.mjs +1 -1
- package/dist/reader.mjs +129 -152
- package/dist/rpc.mjs +1 -1
- package/dist/shared/protocol.d.mts +12 -2
- package/dist/source.d.mts +1 -0
- package/dist/source.mjs +7 -0
- package/dist/{reader.d.mts → sources/hosted.d.mts} +2 -4
- package/dist/sources/hosted.mjs +118 -0
- package/package.json +23 -22
- package/dist/client/assets/index-Bv6TqI10.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Search Console</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-DA7Lgn7_.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BdO8MoYt.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app"></div>
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { DailyPoint, DateWindow, GscdumpContext, MetricTotals, PageStats, PageStatsInput, Period, QueryRow, SiteSummary } from "./shared/protocol.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { HostedSourceOptions } from "./sources/hosted.mjs";
|
|
3
3
|
import { DevframeDefinition } from "devframe";
|
|
4
|
-
export interface GscdumpDevframeOptions extends
|
|
4
|
+
export interface GscdumpDevframeOptions extends HostedSourceOptions {
|
|
5
|
+
/** The Site to read: a Site ID, a Site URL, or its host. Optional when the credential holds one Site. */
|
|
6
|
+
site?: string;
|
|
5
7
|
/** Override the request implementation, for proxies and tests. */
|
|
6
8
|
fetch?: typeof fetch;
|
|
7
9
|
}
|
|
@@ -13,4 +15,4 @@ export interface GscdumpDevframeOptions extends GscdumpReaderOptions {
|
|
|
13
15
|
* devframe hub.
|
|
14
16
|
*/
|
|
15
17
|
export declare function createGscdumpDevframe(options?: GscdumpDevframeOptions): DevframeDefinition;
|
|
16
|
-
export { type DailyPoint, type DateWindow, type GscdumpContext, type
|
|
18
|
+
export { type DailyPoint, type DateWindow, type GscdumpContext, type HostedSourceOptions, type MetricTotals, type PageStats, type PageStatsInput, type Period, type QueryRow, type SiteSummary, createGscdumpDevframe as default };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { description, homepage, name, version } from "./package.mjs";
|
|
2
2
|
import { DEVFRAME_ID } from "./shared/protocol.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { createPageStatsReader } from "./reader.mjs";
|
|
4
4
|
import { createRpcFunctions } from "./rpc.mjs";
|
|
5
|
+
import { createHostedSource } from "./sources/hosted.mjs";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import { readFile } from "node:fs/promises";
|
|
7
8
|
import { homedir } from "node:os";
|
|
@@ -38,12 +39,12 @@ function createGscdumpDevframe(options = {}) {
|
|
|
38
39
|
capabilities: { build: false },
|
|
39
40
|
setup(ctx) {
|
|
40
41
|
const env = process.env;
|
|
41
|
-
const
|
|
42
|
+
const source = createHostedSource(options, {
|
|
42
43
|
fetch: options.fetch ?? globalThis.fetch,
|
|
43
44
|
env,
|
|
44
|
-
readCliAuthentication: () => readCliAuthentication(env)
|
|
45
|
-
now: Date.now
|
|
45
|
+
readCliAuthentication: () => readCliAuthentication(env)
|
|
46
46
|
});
|
|
47
|
+
const reader = createPageStatsReader({ site: options.site }, source, Date.now);
|
|
47
48
|
const scoped = ctx.scope(DEVFRAME_ID);
|
|
48
49
|
for (const fn of createRpcFunctions(reader)) scoped.rpc.register(fn);
|
|
49
50
|
}
|
package/dist/package.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
var name = "@gscdump/devframe";
|
|
2
|
-
var version = "5.
|
|
2
|
+
var version = "5.8.0";
|
|
3
3
|
var description = "Search Console clicks, impressions, and queries for the page you are developing, in Vite DevTools and any devframe hub.";
|
|
4
4
|
var homepage = "https://github.com/harlan-zw/gscdump/tree/main/packages/devframe#readme";
|
|
5
5
|
export { description, homepage, name, version };
|
package/dist/reader.mjs
CHANGED
|
@@ -1,190 +1,172 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { matchSite
|
|
1
|
+
import { hostOf } from "./source.mjs";
|
|
2
|
+
import { matchSite } from "./account.mjs";
|
|
3
3
|
import { addDays, dashboardPageUrl, pagePathOf, parseQueryRows, periodWindows, summariseDaily } from "./page-stats.mjs";
|
|
4
4
|
import { PERIODS } from "./shared/protocol.mjs";
|
|
5
|
-
import { parseRecordReadRefusal } from "@gscdump/contracts";
|
|
6
5
|
import { periodToDateRange } from "@gscdump/sdk/period";
|
|
7
|
-
|
|
8
|
-
const ACCOUNT_TTL_MS = 3e5;
|
|
6
|
+
const SITES_TTL_MS = 3e5;
|
|
9
7
|
const STATS_TTL_MS = 3e5;
|
|
10
8
|
const TOP_QUERIES = 25;
|
|
11
|
-
function rowsBody(dimension, path, window) {
|
|
12
|
-
return {
|
|
13
|
-
dimensions: [dimension],
|
|
14
|
-
filter: { _filters: [{
|
|
15
|
-
dimension: "date",
|
|
16
|
-
operator: "between",
|
|
17
|
-
expression: window.start,
|
|
18
|
-
expression2: window.end
|
|
19
|
-
}, {
|
|
20
|
-
dimension: "page",
|
|
21
|
-
operator: "equals",
|
|
22
|
-
expression: path
|
|
23
|
-
}] },
|
|
24
|
-
orderBy: dimension === "date" ? {
|
|
25
|
-
column: "date",
|
|
26
|
-
dir: "asc"
|
|
27
|
-
} : {
|
|
28
|
-
column: "clicks",
|
|
29
|
-
dir: "desc"
|
|
30
|
-
},
|
|
31
|
-
rowLimit: dimension === "date" ? 400 : TOP_QUERIES
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
9
|
function periodDays(period) {
|
|
35
10
|
return PERIODS.find((option) => option.value === period)?.days ?? 28;
|
|
36
11
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
12
|
+
function contextForPage(sites, pageUrl, preferredSiteId) {
|
|
13
|
+
const host = hostOf(pageUrl) ?? pageUrl;
|
|
14
|
+
const matches = sites.filter((site) => site.host === host);
|
|
15
|
+
if (matches.length === 0) return {
|
|
16
|
+
_tag: "NoSiteForPage",
|
|
17
|
+
host
|
|
18
|
+
};
|
|
19
|
+
const pick = matches.find((site) => site.siteId === preferredSiteId) ?? (matches.length === 1 ? matches[0] : void 0);
|
|
20
|
+
return pick ? {
|
|
21
|
+
_tag: "Ready",
|
|
22
|
+
site: pick,
|
|
23
|
+
sites: matches,
|
|
24
|
+
configured: false
|
|
25
|
+
} : {
|
|
26
|
+
_tag: "SiteRequired",
|
|
27
|
+
sites: matches,
|
|
28
|
+
target: host
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function createPageStatsReader(options, source, now) {
|
|
32
|
+
let sitesCache = null;
|
|
39
33
|
const stats = /* @__PURE__ */ new Map();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
readCliAuthentication: deps.readCliAuthentication
|
|
46
|
-
});
|
|
47
|
-
if (!credential) return {
|
|
48
|
-
_tag: "Blocked",
|
|
49
|
-
context: { _tag: "CredentialMissing" }
|
|
50
|
-
};
|
|
51
|
-
const account = await readAccount(credential, deps.fetch);
|
|
52
|
-
if (account._tag === "Rejected") return {
|
|
53
|
-
_tag: "Blocked",
|
|
54
|
-
context: {
|
|
55
|
-
_tag: "CredentialRejected",
|
|
56
|
-
source: credential.source
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
if (account._tag === "Unavailable") return {
|
|
60
|
-
_tag: "Blocked",
|
|
61
|
-
context: {
|
|
62
|
-
_tag: "Unavailable",
|
|
63
|
-
message: account.message
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
if (account.sites.length === 0) return {
|
|
67
|
-
_tag: "Blocked",
|
|
68
|
-
context: { _tag: "NoSites" }
|
|
69
|
-
};
|
|
70
|
-
return {
|
|
71
|
-
_tag: "Ready",
|
|
72
|
-
credential,
|
|
73
|
-
client: createGscdumpV1Client({
|
|
74
|
-
apiRoot: credential.apiRoot,
|
|
75
|
-
credential: bearerOf(credential),
|
|
76
|
-
fetch: deps.fetch
|
|
77
|
-
}),
|
|
78
|
-
sites: account.sites
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function currentSession() {
|
|
82
|
-
if (session && deps.now() - session.at < ACCOUNT_TTL_MS) return session.value;
|
|
83
|
-
const value = loadSession();
|
|
84
|
-
session = {
|
|
85
|
-
at: deps.now(),
|
|
34
|
+
function readSites() {
|
|
35
|
+
if (sitesCache && now() - sitesCache.at < SITES_TTL_MS) return sitesCache.value;
|
|
36
|
+
const value = source.sites();
|
|
37
|
+
sitesCache = {
|
|
38
|
+
at: now(),
|
|
86
39
|
value
|
|
87
40
|
};
|
|
88
41
|
value.then((result) => {
|
|
89
|
-
if (result._tag === "Blocked" &&
|
|
42
|
+
if (result._tag === "Blocked" && sitesCache?.value === value) sitesCache = null;
|
|
90
43
|
}, () => {
|
|
91
|
-
if (
|
|
44
|
+
if (sitesCache?.value === value) sitesCache = null;
|
|
92
45
|
});
|
|
93
46
|
return value;
|
|
94
47
|
}
|
|
95
|
-
async function context(preferredSiteId) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
48
|
+
async function context(preferredSiteId, pageUrl) {
|
|
49
|
+
const read = await readSites();
|
|
50
|
+
if (read._tag === "Blocked") return read.context;
|
|
51
|
+
const sites = read.sites;
|
|
52
|
+
if (sites.length === 0) return { _tag: "NoSites" };
|
|
98
53
|
if (options.site) {
|
|
99
|
-
const match = matchSite(
|
|
100
|
-
|
|
54
|
+
const match = matchSite(sites, options.site);
|
|
55
|
+
if (match._tag === "Found") return {
|
|
101
56
|
_tag: "Ready",
|
|
102
57
|
site: match.site,
|
|
103
|
-
sites
|
|
58
|
+
sites,
|
|
104
59
|
configured: true
|
|
60
|
+
};
|
|
61
|
+
const picked = preferredSiteId ? sites.find((site) => site.siteId === preferredSiteId) : void 0;
|
|
62
|
+
return picked ? {
|
|
63
|
+
_tag: "Ready",
|
|
64
|
+
site: picked,
|
|
65
|
+
sites,
|
|
66
|
+
configured: false
|
|
105
67
|
} : {
|
|
106
68
|
_tag: "SiteRequired",
|
|
107
|
-
sites
|
|
69
|
+
sites,
|
|
108
70
|
target: options.site
|
|
109
71
|
};
|
|
110
72
|
}
|
|
111
|
-
|
|
73
|
+
if (options.siteFromPage && pageUrl) return contextForPage(sites, pageUrl, preferredSiteId ?? null);
|
|
74
|
+
const preferred = preferredSiteId ? sites.find((site) => site.siteId === preferredSiteId) : void 0;
|
|
112
75
|
if (preferred) return {
|
|
113
76
|
_tag: "Ready",
|
|
114
77
|
site: preferred,
|
|
115
|
-
sites
|
|
78
|
+
sites,
|
|
116
79
|
configured: false
|
|
117
80
|
};
|
|
118
|
-
const match = matchSite(
|
|
81
|
+
const match = matchSite(sites, null);
|
|
119
82
|
return match._tag === "Found" ? {
|
|
120
83
|
_tag: "Ready",
|
|
121
84
|
site: match.site,
|
|
122
|
-
sites
|
|
85
|
+
sites,
|
|
123
86
|
configured: false
|
|
124
87
|
} : {
|
|
125
88
|
_tag: "SiteRequired",
|
|
126
|
-
sites
|
|
89
|
+
sites,
|
|
90
|
+
target: null
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function siteForPage(input) {
|
|
94
|
+
if (!options.siteFromPage) return context(input.siteId);
|
|
95
|
+
const read = await readSites();
|
|
96
|
+
if (read._tag === "Blocked") return read.context;
|
|
97
|
+
const chosen = input.siteId ? read.sites.find((site) => site.siteId === input.siteId) : void 0;
|
|
98
|
+
if (chosen) return {
|
|
99
|
+
_tag: "Ready",
|
|
100
|
+
site: chosen,
|
|
101
|
+
sites: read.sites,
|
|
102
|
+
configured: false
|
|
103
|
+
};
|
|
104
|
+
return /^https?:\/\//i.test(input.page) ? contextForPage(read.sites, input.page, null) : {
|
|
105
|
+
_tag: "SiteRequired",
|
|
106
|
+
sites: read.sites,
|
|
127
107
|
target: null
|
|
128
108
|
};
|
|
129
109
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
body
|
|
134
|
-
})).data.rows;
|
|
110
|
+
function firstFailure(daily, queries) {
|
|
111
|
+
if (daily._tag !== "Ok") return daily;
|
|
112
|
+
return queries._tag !== "Ok" ? queries : null;
|
|
135
113
|
}
|
|
136
|
-
async function readPage(
|
|
114
|
+
async function readPage(site, path, period, windows, retried) {
|
|
137
115
|
const span = {
|
|
138
116
|
start: windows.previous?.start ?? windows.current.start,
|
|
139
117
|
end: windows.current.end
|
|
140
118
|
};
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
_tag: "Ok",
|
|
146
|
-
siteId: site.siteId,
|
|
147
|
-
path,
|
|
148
|
-
period,
|
|
149
|
-
window: windows.current,
|
|
150
|
-
previousWindow: windows.previous,
|
|
151
|
-
...summariseDaily(daily, windows),
|
|
152
|
-
queries: parseQueryRows(queries),
|
|
153
|
-
dashboardUrl: dashboardPageUrl(dashboardOrigin(current.credential.apiRoot), site, path)
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
if (!isGscdumpV1Error(result)) throw result;
|
|
157
|
-
const refusal = parseRecordReadRefusal(result.details);
|
|
158
|
-
if (refusal?.reason === "range_not_synced" && !retried && refusal.missingStart > windows.current.start) {
|
|
159
|
-
const shifted = periodWindows(periodDays(period), {
|
|
160
|
-
oldestDate: site.oldestDate,
|
|
161
|
-
newestDate: addDays(refusal.missingStart, -1)
|
|
162
|
-
});
|
|
163
|
-
if (shifted) return readPage(current, site, path, period, shifted, true);
|
|
164
|
-
}
|
|
165
|
-
if (refusal?.reason === "record_not_ready") return {
|
|
166
|
-
_tag: "NoData",
|
|
119
|
+
const [daily, queries] = await Promise.all([source.dailyRows(site.siteId, path, span), source.queryRows(site.siteId, path, windows.current, TOP_QUERIES)]);
|
|
120
|
+
const failure = firstFailure(daily, queries);
|
|
121
|
+
if (!failure && daily._tag === "Ok" && queries._tag === "Ok") return {
|
|
122
|
+
_tag: "Ok",
|
|
167
123
|
siteId: site.siteId,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
124
|
+
path,
|
|
125
|
+
period,
|
|
126
|
+
window: windows.current,
|
|
127
|
+
previousWindow: windows.previous,
|
|
128
|
+
...summariseDaily(daily.rows, windows),
|
|
129
|
+
queries: parseQueryRows(queries.rows),
|
|
130
|
+
dashboardUrl: dashboardPageUrl(source.dashboardOrigin, site, path)
|
|
131
|
+
};
|
|
132
|
+
switch (failure?._tag) {
|
|
133
|
+
case "Refused": {
|
|
134
|
+
const refusal = failure.refusal;
|
|
135
|
+
if (refusal.reason === "range_not_synced" && !retried && refusal.missingStart > windows.current.start) {
|
|
136
|
+
const shifted = periodWindows(periodDays(period), {
|
|
137
|
+
oldestDate: site.oldestDate,
|
|
138
|
+
newestDate: addDays(refusal.missingStart, -1)
|
|
139
|
+
});
|
|
140
|
+
if (shifted) return readPage(site, path, period, shifted, true);
|
|
141
|
+
}
|
|
142
|
+
return refusal.reason === "record_not_ready" ? {
|
|
143
|
+
_tag: "NoData",
|
|
144
|
+
siteId: site.siteId,
|
|
145
|
+
message: "gscdump has not prepared this Site's record for reads yet. Try again in a few minutes."
|
|
146
|
+
} : {
|
|
147
|
+
_tag: "NoData",
|
|
148
|
+
siteId: site.siteId,
|
|
149
|
+
message: `The Site's record does not hold ${refusal.missingStart} to ${refusal.missingEnd} yet.`
|
|
150
|
+
};
|
|
181
151
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
152
|
+
case "RateLimited": return { _tag: "RateLimited" };
|
|
153
|
+
case "Unauthorized":
|
|
154
|
+
sitesCache = null;
|
|
155
|
+
return {
|
|
156
|
+
_tag: "SiteUnavailable",
|
|
157
|
+
context: failure.context
|
|
158
|
+
};
|
|
159
|
+
case "Failed": return {
|
|
160
|
+
_tag: "Failed",
|
|
161
|
+
message: failure.message,
|
|
162
|
+
requestId: failure.requestId
|
|
163
|
+
};
|
|
164
|
+
default: return {
|
|
165
|
+
_tag: "Failed",
|
|
166
|
+
message: "The read returned no rows and no error.",
|
|
167
|
+
requestId: null
|
|
168
|
+
};
|
|
169
|
+
}
|
|
188
170
|
}
|
|
189
171
|
async function pageStats(input) {
|
|
190
172
|
const path = pagePathOf(input.page);
|
|
@@ -193,7 +175,7 @@ function createGscdumpReader(options, deps) {
|
|
|
193
175
|
page: input.page
|
|
194
176
|
};
|
|
195
177
|
const period = input.period ?? "28d";
|
|
196
|
-
const resolved = await
|
|
178
|
+
const resolved = await siteForPage(input);
|
|
197
179
|
if (resolved._tag !== "Ready") return {
|
|
198
180
|
_tag: "SiteUnavailable",
|
|
199
181
|
context: resolved
|
|
@@ -201,13 +183,8 @@ function createGscdumpReader(options, deps) {
|
|
|
201
183
|
const site = resolved.site;
|
|
202
184
|
const key = `${site.siteId}|${period}|${path}`;
|
|
203
185
|
const hit = stats.get(key);
|
|
204
|
-
if (hit &&
|
|
205
|
-
const
|
|
206
|
-
if (current._tag !== "Ready") return {
|
|
207
|
-
_tag: "SiteUnavailable",
|
|
208
|
-
context: current.context
|
|
209
|
-
};
|
|
210
|
-
const stableEnd = periodToDateRange(period, { now: new Date(deps.now()) }).end;
|
|
186
|
+
if (hit && now() - hit.at < STATS_TTL_MS) return hit.value;
|
|
187
|
+
const stableEnd = periodToDateRange(period, { now: new Date(now()) }).end;
|
|
211
188
|
const newestDate = site.newestDate && site.newestDate > stableEnd ? stableEnd : site.newestDate;
|
|
212
189
|
const windows = periodWindows(periodDays(period), {
|
|
213
190
|
oldestDate: site.oldestDate,
|
|
@@ -218,9 +195,9 @@ function createGscdumpReader(options, deps) {
|
|
|
218
195
|
siteId: site.siteId,
|
|
219
196
|
message: "The Site's record holds no days yet. gscdump is still syncing it."
|
|
220
197
|
};
|
|
221
|
-
const value = readPage(
|
|
198
|
+
const value = readPage(site, path, period, windows, false);
|
|
222
199
|
stats.set(key, {
|
|
223
|
-
at:
|
|
200
|
+
at: now(),
|
|
224
201
|
value
|
|
225
202
|
});
|
|
226
203
|
value.then((result) => {
|
|
@@ -235,4 +212,4 @@ function createGscdumpReader(options, deps) {
|
|
|
235
212
|
pageStats
|
|
236
213
|
};
|
|
237
214
|
}
|
|
238
|
-
export {
|
|
215
|
+
export { createPageStatsReader };
|
package/dist/rpc.mjs
CHANGED
|
@@ -15,7 +15,7 @@ function createRpcFunctions(reader) {
|
|
|
15
15
|
name: "get-context",
|
|
16
16
|
type: "query",
|
|
17
17
|
jsonSerializable: true,
|
|
18
|
-
handler: (preferredSiteId) => reader.context(preferredSiteId ?? null)
|
|
18
|
+
handler: (preferredSiteId, pageUrl) => reader.context(preferredSiteId ?? null, pageUrl ?? null)
|
|
19
19
|
}), defineRpcFunction({
|
|
20
20
|
name: "get-page-stats",
|
|
21
21
|
type: "query",
|
|
@@ -3,14 +3,16 @@ export interface SiteSummary {
|
|
|
3
3
|
siteId: string;
|
|
4
4
|
/** The Site as gscdump registered it, for example `example.com`. */
|
|
5
5
|
siteUrl: string;
|
|
6
|
+
/** The exact host the Site covers, for example `example.com`. A page on another host is not on this Site. */
|
|
7
|
+
host: string;
|
|
6
8
|
/** The first day the Hosted record holds, `YYYY-MM-DD`. */
|
|
7
9
|
oldestDate: string | null;
|
|
8
10
|
/** The last day the Hosted record holds, `YYYY-MM-DD`. */
|
|
9
11
|
newestDate: string | null;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
|
-
* What the
|
|
13
|
-
* the next step the panel shows.
|
|
14
|
+
* What the panel can read, decided once per panel boot and again when the
|
|
15
|
+
* page's host changes. Each tag names the next step the panel shows.
|
|
14
16
|
*/
|
|
15
17
|
export type GscdumpContext = {
|
|
16
18
|
_tag: 'Ready';
|
|
@@ -22,12 +24,20 @@ export type GscdumpContext = {
|
|
|
22
24
|
sites: SiteSummary[];
|
|
23
25
|
target: string | null;
|
|
24
26
|
} | {
|
|
27
|
+
_tag: 'NoSiteForPage';
|
|
28
|
+
host: string;
|
|
29
|
+
} | BlockedContext;
|
|
30
|
+
/** A context that stops before any Site is chosen: no credential, no Sites, or no service. */
|
|
31
|
+
export type BlockedContext = {
|
|
25
32
|
_tag: 'NoSites';
|
|
26
33
|
} | {
|
|
27
34
|
_tag: 'CredentialMissing';
|
|
28
35
|
} | {
|
|
29
36
|
_tag: 'CredentialRejected';
|
|
30
37
|
source: CredentialSource;
|
|
38
|
+
} | {
|
|
39
|
+
_tag: 'SignedOut';
|
|
40
|
+
signInUrl: string;
|
|
31
41
|
} | {
|
|
32
42
|
_tag: 'Unavailable';
|
|
33
43
|
message: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./shared/protocol.mjs";
|
package/dist/source.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
function hostOf(value) {
|
|
2
|
+
const text = value.trim();
|
|
3
|
+
if (text.startsWith("sc-domain:")) return text.slice(10).toLowerCase().replace(/\.$/, "") || null;
|
|
4
|
+
const url = URL.parse(/^[a-z][a-z\d+.-]*:\/\//i.test(text) ? text : `https://${text}`);
|
|
5
|
+
return url?.hostname ? url.hostname.toLowerCase() : null;
|
|
6
|
+
}
|
|
7
|
+
export { hostOf };
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import "
|
|
2
|
-
export interface
|
|
3
|
-
/** The Site to read: a Site ID, a Site URL, or its host. Optional when the credential holds one Site. */
|
|
4
|
-
site?: string;
|
|
1
|
+
import "../source.mjs";
|
|
2
|
+
export interface HostedSourceOptions {
|
|
5
3
|
/** A gscdump user API key. Defaults to `GSCDUMP_API_KEY`, then the CLI's Hosted login. */
|
|
6
4
|
apiKey?: string;
|
|
7
5
|
/** Defaults to `GSCDUMP_API_ROOT`, then `https://gscdump.com/api`. */
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { bearerOf, dashboardOrigin, resolveCredential } from "../credential.mjs";
|
|
2
|
+
import { readAccount } from "../account.mjs";
|
|
3
|
+
import { parseRecordReadRefusal } from "@gscdump/contracts";
|
|
4
|
+
import { createGscdumpV1Client, isGscdumpV1Error } from "@gscdump/sdk/v1";
|
|
5
|
+
function rowsBody(dimension, path, window, rowLimit) {
|
|
6
|
+
return {
|
|
7
|
+
dimensions: [dimension],
|
|
8
|
+
filter: { _filters: [{
|
|
9
|
+
dimension: "date",
|
|
10
|
+
operator: "between",
|
|
11
|
+
expression: window.start,
|
|
12
|
+
expression2: window.end
|
|
13
|
+
}, {
|
|
14
|
+
dimension: "page",
|
|
15
|
+
operator: "equals",
|
|
16
|
+
expression: path
|
|
17
|
+
}] },
|
|
18
|
+
orderBy: dimension === "date" ? {
|
|
19
|
+
column: "date",
|
|
20
|
+
dir: "asc"
|
|
21
|
+
} : {
|
|
22
|
+
column: "clicks",
|
|
23
|
+
dir: "desc"
|
|
24
|
+
},
|
|
25
|
+
rowLimit
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createHostedSource(options, deps) {
|
|
29
|
+
const apiRoot = (options.apiRoot ?? deps.env.GSCDUMP_API_ROOT ?? "https://gscdump.com/api").replace(/\/+$/, "");
|
|
30
|
+
let ready = null;
|
|
31
|
+
async function sites() {
|
|
32
|
+
const read = await readSites();
|
|
33
|
+
if (read._tag === "Blocked") ready = null;
|
|
34
|
+
return read;
|
|
35
|
+
}
|
|
36
|
+
async function readSites() {
|
|
37
|
+
const credential = await resolveCredential({
|
|
38
|
+
apiKey: options.apiKey,
|
|
39
|
+
apiRoot: options.apiRoot,
|
|
40
|
+
env: deps.env,
|
|
41
|
+
readCliAuthentication: deps.readCliAuthentication
|
|
42
|
+
});
|
|
43
|
+
if (!credential) return {
|
|
44
|
+
_tag: "Blocked",
|
|
45
|
+
context: { _tag: "CredentialMissing" }
|
|
46
|
+
};
|
|
47
|
+
const account = await readAccount(credential, deps.fetch);
|
|
48
|
+
if (account._tag === "Rejected") return {
|
|
49
|
+
_tag: "Blocked",
|
|
50
|
+
context: {
|
|
51
|
+
_tag: "CredentialRejected",
|
|
52
|
+
source: credential.source
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
if (account._tag === "Unavailable") return {
|
|
56
|
+
_tag: "Blocked",
|
|
57
|
+
context: {
|
|
58
|
+
_tag: "Unavailable",
|
|
59
|
+
message: account.message
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
ready = {
|
|
63
|
+
credential,
|
|
64
|
+
client: createGscdumpV1Client({
|
|
65
|
+
apiRoot: credential.apiRoot,
|
|
66
|
+
credential: bearerOf(credential),
|
|
67
|
+
fetch: deps.fetch
|
|
68
|
+
})
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
_tag: "Ok",
|
|
72
|
+
sites: account.sites
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function readRows(siteId, body) {
|
|
76
|
+
if (!ready) return {
|
|
77
|
+
_tag: "Unauthorized",
|
|
78
|
+
context: { _tag: "CredentialMissing" }
|
|
79
|
+
};
|
|
80
|
+
const { client, credential } = ready;
|
|
81
|
+
return client.queryAnalyticsRows({
|
|
82
|
+
params: { siteId },
|
|
83
|
+
body
|
|
84
|
+
}).then((response) => ({
|
|
85
|
+
_tag: "Ok",
|
|
86
|
+
rows: response.data.rows
|
|
87
|
+
}), (error) => {
|
|
88
|
+
if (!isGscdumpV1Error(error)) throw error;
|
|
89
|
+
const refusal = parseRecordReadRefusal(error.details);
|
|
90
|
+
if (refusal) return {
|
|
91
|
+
_tag: "Refused",
|
|
92
|
+
refusal
|
|
93
|
+
};
|
|
94
|
+
if (error.code === "rate_limited") return { _tag: "RateLimited" };
|
|
95
|
+
if (error.status === 401 || error.status === 403) return {
|
|
96
|
+
_tag: "Unauthorized",
|
|
97
|
+
context: {
|
|
98
|
+
_tag: "CredentialRejected",
|
|
99
|
+
source: credential.source
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
_tag: "Failed",
|
|
104
|
+
message: error.message,
|
|
105
|
+
requestId: error.requestId ?? null
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
sites,
|
|
111
|
+
dailyRows: (siteId, path, window) => readRows(siteId, rowsBody("date", path, window, 400)),
|
|
112
|
+
queryRows: (siteId, path, window, limit) => readRows(siteId, rowsBody("query", path, window, limit)),
|
|
113
|
+
get dashboardOrigin() {
|
|
114
|
+
return dashboardOrigin(ready?.credential.apiRoot ?? apiRoot);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export { createHostedSource };
|