@speedkit/cli 4.26.0 → 4.27.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/CHANGELOG.md +17 -0
- package/README.md +203 -15
- package/dist/commands/auto-prewarm.d.ts +12 -1
- package/dist/commands/auto-prewarm.js +26 -7
- package/dist/commands/generate-pop-config.d.ts +1 -0
- package/dist/commands/generate-pop-config.js +9 -3
- package/dist/commands/prewarm.js +3 -1
- package/dist/commands/query/parameter.d.ts +27 -0
- package/dist/commands/query/parameter.js +28 -0
- package/dist/commands/query/prewarm.d.ts +32 -0
- package/dist/commands/query/prewarm.js +32 -0
- package/dist/helpers/clipboard.d.ts +19 -0
- package/dist/helpers/clipboard.js +70 -0
- package/dist/helpers/evaluate-speed-kit-config.d.ts +8 -18
- package/dist/helpers/evaluate-speed-kit-config.js +8 -6
- package/dist/helpers/evaluate-speed-kit-config.spec.d.ts +1 -0
- package/dist/helpers/evaluate-speed-kit-config.spec.js +78 -0
- package/dist/models/cli-parameters.d.ts +106 -0
- package/dist/models/cli-parameters.js +183 -0
- package/dist/models/cli-parameters.spec.d.ts +1 -0
- package/dist/models/cli-parameters.spec.js +47 -0
- package/dist/services/athena/athena-service-factory.d.ts +4 -1
- package/dist/services/athena/athena-service-factory.js +9 -3
- package/dist/services/athena/athena-service.d.ts +13 -1
- package/dist/services/athena/athena-service.js +27 -5
- package/dist/services/athena/athena-service.spec.d.ts +1 -0
- package/dist/services/athena/athena-service.spec.js +93 -0
- package/dist/services/deploy/handler/install-resource-handler.js +3 -4
- package/dist/services/onboarding/onboarding-service-factory.js +2 -2
- package/dist/services/pop-config/pop-config-service.d.ts +8 -1
- package/dist/services/pop-config/pop-config-service.js +16 -28
- package/dist/services/prewarm/assets/asset-api-client.d.ts +1 -1
- package/dist/services/prewarm/assets/asset-api-client.js +5 -4
- package/dist/services/prewarm/auto-pre-warm-factory.d.ts +20 -2
- package/dist/services/prewarm/auto-pre-warm-factory.js +24 -6
- package/dist/services/prewarm/csv-reader.d.ts +21 -2
- package/dist/services/prewarm/csv-reader.js +71 -9
- package/dist/services/prewarm/csv-reader.spec.d.ts +1 -0
- package/dist/services/prewarm/csv-reader.spec.js +75 -0
- package/dist/services/prewarm/index.d.ts +1 -0
- package/dist/services/prewarm/index.js +1 -0
- package/dist/services/prewarm/pre-warm-factory.d.ts +0 -2
- package/dist/services/prewarm/pre-warm-factory.js +8 -11
- package/dist/services/prewarm/pre-warm-model.d.ts +14 -1
- package/dist/services/prewarm/pre-warm-model.js +0 -1
- package/dist/services/prewarm/pre-warm-service.d.ts +9 -4
- package/dist/services/prewarm/pre-warm-service.js +18 -14
- package/dist/services/prewarm/prewarm-targets.d.ts +12 -0
- package/dist/services/prewarm/prewarm-targets.js +18 -0
- package/dist/services/prewarm/prewarm-targets.spec.d.ts +1 -0
- package/dist/services/prewarm/prewarm-targets.spec.js +29 -0
- package/dist/services/query-builder/error/match-matches-nothing-error.d.ts +11 -0
- package/dist/services/query-builder/error/match-matches-nothing-error.js +17 -0
- package/dist/services/query-builder/queries/device.d.ts +21 -0
- package/dist/services/query-builder/queries/device.js +26 -0
- package/dist/services/query-builder/queries/index.d.ts +6 -0
- package/dist/services/query-builder/queries/index.js +6 -0
- package/dist/services/query-builder/queries/page-filter.d.ts +39 -0
- package/dist/services/query-builder/queries/page-filter.js +135 -0
- package/dist/services/query-builder/queries/page-source.d.ts +30 -0
- package/dist/services/query-builder/queries/page-source.js +79 -0
- package/dist/services/query-builder/queries/parameter-query.d.ts +14 -0
- package/dist/services/query-builder/queries/parameter-query.js +142 -0
- package/dist/services/query-builder/queries/pops-query.d.ts +27 -0
- package/dist/services/query-builder/queries/pops-query.js +65 -0
- package/dist/services/query-builder/queries/prewarm-query.d.ts +18 -0
- package/dist/services/query-builder/queries/prewarm-query.js +227 -0
- package/dist/services/query-builder/queries/query-model.d.ts +97 -0
- package/dist/services/query-builder/queries/query-model.js +19 -0
- package/dist/services/query-builder/queries/spec/golden-configs.d.ts +18 -0
- package/dist/services/query-builder/queries/spec/golden-configs.js +116 -0
- package/dist/services/query-builder/queries/spec/golden.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/golden.spec.js +161 -0
- package/dist/services/query-builder/queries/spec/pops-query.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/pops-query.spec.js +58 -0
- package/dist/services/query-builder/queries/spec/queries.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/queries.spec.js +312 -0
- package/dist/services/query-builder/queries/spec/reported-bugs.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/reported-bugs.spec.js +86 -0
- package/dist/services/query-builder/query-builder-factory.d.ts +2 -2
- package/dist/services/query-builder/query-builder-factory.js +36 -8
- package/dist/services/query-builder/query-builder-model.d.ts +33 -19
- package/dist/services/query-builder/query-builder-model.js +5 -5
- package/dist/services/query-builder/query-builder-service.d.ts +26 -5
- package/dist/services/query-builder/query-builder-service.js +157 -44
- package/dist/services/query-builder/query-command.d.ts +27 -0
- package/dist/services/query-builder/query-command.js +36 -0
- package/dist/services/query-builder/rules/index.d.ts +8 -0
- package/dist/services/query-builder/rules/index.js +8 -0
- package/dist/services/query-builder/rules/match-pattern.d.ts +51 -0
- package/dist/services/query-builder/rules/match-pattern.js +141 -0
- package/dist/services/query-builder/rules/pattern-language.d.ts +59 -0
- package/dist/services/query-builder/rules/pattern-language.js +159 -0
- package/dist/services/query-builder/rules/rule-model.d.ts +107 -0
- package/dist/services/query-builder/rules/rule-model.js +54 -0
- package/dist/services/query-builder/rules/rule-parser.d.ts +33 -0
- package/dist/services/query-builder/rules/rule-parser.js +189 -0
- package/dist/services/query-builder/rules/rule-reducer.d.ts +71 -0
- package/dist/services/query-builder/rules/rule-reducer.js +383 -0
- package/dist/services/query-builder/rules/rule-relevance.d.ts +43 -0
- package/dist/services/query-builder/rules/rule-relevance.js +69 -0
- package/dist/services/query-builder/rules/rule-to-sql.d.ts +51 -0
- package/dist/services/query-builder/rules/rule-to-sql.js +75 -0
- package/dist/services/query-builder/rules/spec/pattern-language.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/pattern-language.spec.js +90 -0
- package/dist/services/query-builder/rules/spec/rule-parser.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-parser.spec.js +150 -0
- package/dist/services/query-builder/rules/spec/rule-reducer.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-reducer.spec.js +169 -0
- package/dist/services/query-builder/rules/spec/rule-relevance.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-relevance.spec.js +88 -0
- package/dist/services/query-builder/rules/spec/rule-to-sql.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-to-sql.spec.js +110 -0
- package/dist/services/query-builder/rules/spec/runtime-semantics.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/runtime-semantics.spec.js +55 -0
- package/dist/services/query-builder/rules/spec/strip-parameters.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/strip-parameters.spec.js +85 -0
- package/dist/services/query-builder/rules/strip-parameters.d.ts +39 -0
- package/dist/services/query-builder/rules/strip-parameters.js +197 -0
- package/dist/services/query-builder/sql/expression.d.ts +73 -0
- package/dist/services/query-builder/sql/expression.js +133 -0
- package/dist/services/query-builder/sql/format.d.ts +14 -0
- package/dist/services/query-builder/sql/format.js +48 -0
- package/dist/services/query-builder/sql/index.d.ts +4 -0
- package/dist/services/query-builder/sql/index.js +4 -0
- package/dist/services/query-builder/sql/predicate.d.ts +66 -0
- package/dist/services/query-builder/sql/predicate.js +124 -0
- package/dist/services/query-builder/sql/regex.d.ts +28 -0
- package/dist/services/query-builder/sql/regex.js +81 -0
- package/dist/services/query-builder/sql/spec/expression.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/expression.spec.js +75 -0
- package/dist/services/query-builder/sql/spec/format.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/format.spec.js +37 -0
- package/dist/services/query-builder/sql/spec/predicate.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/predicate.spec.js +78 -0
- package/dist/services/query-builder/sql/spec/regex.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/regex.spec.js +62 -0
- package/oclif.manifest.json +401 -100
- package/package.json +11 -2
- package/dist/commands/build-parameter-query.d.ts +0 -14
- package/dist/commands/build-parameter-query.js +0 -25
- package/dist/commands/build-prewarm-query.d.ts +0 -16
- package/dist/commands/build-prewarm-query.js +0 -37
- package/dist/helpers/build-query-helper.d.ts +0 -32
- package/dist/helpers/build-query-helper.js +0 -223
- package/dist/helpers/get-parsed-config.d.ts +0 -15
- package/dist/helpers/get-parsed-config.js +0 -62
- package/dist/services/onboarding/dashboard/parameter-query-builder.d.ts +0 -8
- package/dist/services/onboarding/dashboard/parameter-query-builder.js +0 -63
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { parseRuleSets } from "../../rules/index.js";
|
|
4
|
+
import { buildParameterQuery } from "../parameter-query.js";
|
|
5
|
+
import { buildPrewarmQuery } from "../prewarm-query.js";
|
|
6
|
+
import { sinceDate } from "../query-model.js";
|
|
7
|
+
function inputFor(config, options = {}) {
|
|
8
|
+
return {
|
|
9
|
+
app: "shop-de",
|
|
10
|
+
days: 10,
|
|
11
|
+
hosts: ["www.shop.de"],
|
|
12
|
+
ruleSets: parseRuleSets(config),
|
|
13
|
+
...options,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
blacklist: [{ contentType: ["image"], pathname: [/\/media\//i] }],
|
|
18
|
+
enabledSites: [{ host: "www.shop.de", pathname: [/^\/p\//i] }],
|
|
19
|
+
ruleSetConfig: { version: 2 },
|
|
20
|
+
stripQueryParams: [{ params: ["gclid"] }],
|
|
21
|
+
whitelist: [{ host: ["www.shop.de"] }],
|
|
22
|
+
};
|
|
23
|
+
/** A config whose enabled scope is wide enough for a guard to survive. */
|
|
24
|
+
const WIDE_CONFIG = {
|
|
25
|
+
disabledScopes: ["/checkout", "/cart"],
|
|
26
|
+
enabledSites: [{ pathname: [/^\//i] }],
|
|
27
|
+
ruleSetConfig: { version: 2 },
|
|
28
|
+
};
|
|
29
|
+
describe("sinceDate", () => {
|
|
30
|
+
it("should count back from the given day", () => {
|
|
31
|
+
expect(sinceDate(10, new Date("2026-09-04T08:00:00Z"))).to.equal("2026-08-25");
|
|
32
|
+
});
|
|
33
|
+
it("should keep a window of at least one day", () => {
|
|
34
|
+
expect(sinceDate(0, new Date("2026-09-04T08:00:00Z"))).to.equal("2026-09-03");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("the row filter of both queries", () => {
|
|
38
|
+
const queries = {
|
|
39
|
+
parameter: buildParameterQuery,
|
|
40
|
+
prewarm: buildPrewarmQuery,
|
|
41
|
+
};
|
|
42
|
+
for (const [name, build] of Object.entries(queries)) {
|
|
43
|
+
describe(name, () => {
|
|
44
|
+
it("should read the app and the window", () => {
|
|
45
|
+
const { sql } = build(inputFor(CONFIG));
|
|
46
|
+
expect(sql).to.contain("app = 'shop-de'");
|
|
47
|
+
expect(sql).to.contain(`date >= '${sinceDate(10)}'`);
|
|
48
|
+
});
|
|
49
|
+
it("should always exclude bots", () => {
|
|
50
|
+
expect(build(inputFor(CONFIG)).sql).to.contain("device <> 'Robot'");
|
|
51
|
+
});
|
|
52
|
+
it("should not filter navigations unless asked to", () => {
|
|
53
|
+
expect(build(inputFor(CONFIG)).sql).to.not.contain("navigationtype");
|
|
54
|
+
});
|
|
55
|
+
it("should keep hard navigations only when asked to", () => {
|
|
56
|
+
const { sql } = build(inputFor(CONFIG, { hardNavigationsOnly: true }));
|
|
57
|
+
expect(sql).to.contain("navigationtype in ('NAVIGATE', 'RELOAD', 'BACK_FORWARD')");
|
|
58
|
+
});
|
|
59
|
+
it("should exclude suspicious page impressions when asked to", () => {
|
|
60
|
+
const { sql } = build(inputFor(CONFIG, { excludeSuspicious: true }));
|
|
61
|
+
// Null has to count as not suspicious, or the rows vanish with it.
|
|
62
|
+
expect(sql).to.contain("not coalesce(suspicious, false)");
|
|
63
|
+
expect(sql).to.not.contain("noindex");
|
|
64
|
+
});
|
|
65
|
+
it("should exclude noindex pages when asked to", () => {
|
|
66
|
+
const { sql } = build(inputFor(CONFIG, { excludeNoindex: true }));
|
|
67
|
+
expect(sql).to.contain("not coalesce(noindex, false)");
|
|
68
|
+
});
|
|
69
|
+
it("should read the relation only through the page source", () => {
|
|
70
|
+
const { sql } = build(inputFor(CONFIG));
|
|
71
|
+
expect(sql.match(/live\.rum\.pi/g)).to.have.lengthOf(1);
|
|
72
|
+
expect(sql).to.contain("from pages");
|
|
73
|
+
});
|
|
74
|
+
it("should leave out a rule that cannot apply to a document", () => {
|
|
75
|
+
const { dropped, sql } = build(inputFor(CONFIG));
|
|
76
|
+
expect(sql).to.not.contain("/media/");
|
|
77
|
+
expect(dropped.map(({ source }) => source)).to.contain("blacklist[0]");
|
|
78
|
+
});
|
|
79
|
+
it("should exclude the pages a disabled scope claims", () => {
|
|
80
|
+
// The service worker never runs there, so Speed Kit never sees them.
|
|
81
|
+
const { sql } = build(inputFor(WIDE_CONFIG));
|
|
82
|
+
expect(sql).to.contain("regexp_like(path, '^/checkout')");
|
|
83
|
+
expect(sql).to.contain("regexp_like(path, '^/cart')");
|
|
84
|
+
});
|
|
85
|
+
it("should reduce away a scope no enabled page can reach", () => {
|
|
86
|
+
const { reduced, sql } = build(inputFor({
|
|
87
|
+
...WIDE_CONFIG,
|
|
88
|
+
enabledSites: [{ pathname: [/^\/p\//i] }],
|
|
89
|
+
}));
|
|
90
|
+
expect(sql).to.not.contain("checkout");
|
|
91
|
+
expect(reduced.map(({ source }) => source)).to.contain("disabledScopes[0]");
|
|
92
|
+
});
|
|
93
|
+
it("should project only the subjects the rules use", () => {
|
|
94
|
+
const { sql } = build(inputFor(CONFIG));
|
|
95
|
+
expect(sql).to.contain("as host");
|
|
96
|
+
expect(sql).to.contain("as path");
|
|
97
|
+
expect(sql).to.not.contain("as pathWithSearch");
|
|
98
|
+
expect(sql).to.not.contain("as matchUrl");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
describe("buildPrewarmQuery", () => {
|
|
104
|
+
it("should group the stripped urls by request count", () => {
|
|
105
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG));
|
|
106
|
+
expect(sql).to.contain("select url, count(*) as hits");
|
|
107
|
+
expect(sql).to.contain("group by url");
|
|
108
|
+
expect(sql).to.contain("order by hits desc");
|
|
109
|
+
});
|
|
110
|
+
it("should keep the whole list unless asked to cut it", () => {
|
|
111
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG));
|
|
112
|
+
expect(sql).to.not.contain("having");
|
|
113
|
+
expect(sql).to.not.contain("limit");
|
|
114
|
+
});
|
|
115
|
+
it("should drop urls below the hit threshold", () => {
|
|
116
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { minHits: 5 }));
|
|
117
|
+
expect(sql).to.contain("having count(*) >= 5");
|
|
118
|
+
});
|
|
119
|
+
it("should keep only the most requested urls when limited", () => {
|
|
120
|
+
expect(buildPrewarmQuery(inputFor(CONFIG, { limit: 5000 })).sql).to.contain("limit 5000");
|
|
121
|
+
});
|
|
122
|
+
it("should drop urls the origin mostly answers with a 404", () => {
|
|
123
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { excludeNotFound: true }));
|
|
124
|
+
expect(sql).to.contain("responsecause");
|
|
125
|
+
expect(sql).to.contain("2 * count_if(responsecause = 'OriginStatusNotFound') < count(*)");
|
|
126
|
+
});
|
|
127
|
+
it("should not read the response cause unless it filters on it", () => {
|
|
128
|
+
expect(buildPrewarmQuery(inputFor(CONFIG)).sql).to.not.contain("responsecause");
|
|
129
|
+
});
|
|
130
|
+
it("should not carry createdat through the prewarm query", () => {
|
|
131
|
+
// Only the parameter query picks example urls by creation time.
|
|
132
|
+
expect(buildPrewarmQuery(inputFor(CONFIG)).sql).to.not.contain("createdat");
|
|
133
|
+
});
|
|
134
|
+
it("should strip the configured parameters from the url", () => {
|
|
135
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG));
|
|
136
|
+
expect(sql).to.contain("(?i)[?&](gclid)(=[^&]*)?(?=&|$)");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("the --device split", () => {
|
|
140
|
+
const DEVICES = inputFor(CONFIG, { devices: ["desktop", "mobile"] });
|
|
141
|
+
it("should read the device classes asked for, and no other", () => {
|
|
142
|
+
const { sql } = buildPrewarmQuery(DEVICES);
|
|
143
|
+
expect(sql).to.contain("device in ('Desktop', 'Mobile')");
|
|
144
|
+
// Naming them subsumes the bot exclusion.
|
|
145
|
+
expect(sql).to.not.contain("device <> 'Robot'");
|
|
146
|
+
expect(sql).to.not.contain("'Tablet'");
|
|
147
|
+
});
|
|
148
|
+
it("should carry the device as the variation of the row", () => {
|
|
149
|
+
expect(buildPrewarmQuery(DEVICES).sql).to.contain("lower(device) as variation");
|
|
150
|
+
});
|
|
151
|
+
it("should count the pairs and rank them by their own hits", () => {
|
|
152
|
+
const { sql } = buildPrewarmQuery(DEVICES);
|
|
153
|
+
expect(sql).to.contain("select impressions.url, impressions.variation, count(*) as hits");
|
|
154
|
+
expect(sql).to.contain("join urls on urls.url = impressions.url");
|
|
155
|
+
expect(sql).to.contain("group by 1, 2");
|
|
156
|
+
expect(sql).to.contain("order by 3 desc");
|
|
157
|
+
});
|
|
158
|
+
it("should still limit urls rather than rows", () => {
|
|
159
|
+
// The limit belongs to the relation that has one row per url, so
|
|
160
|
+
// `--limit 5000` is 5000 pages, not 5000 pre-warm requests.
|
|
161
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { devices: ["desktop"], limit: 5000, minHits: 5 }));
|
|
162
|
+
expect(sql).to.contain("urls as (\n select url, count(*) as hits\n from impressions\n group by url\n having count(*) >= 5\n order by hits desc\n limit 5000\n)");
|
|
163
|
+
});
|
|
164
|
+
it("should ignore the order the devices were given in", () => {
|
|
165
|
+
expect(buildPrewarmQuery(inputFor(CONFIG, { devices: ["mobile", "desktop"] }))
|
|
166
|
+
.sql).to.equal(buildPrewarmQuery(DEVICES).sql);
|
|
167
|
+
});
|
|
168
|
+
it("should not read the device unless it splits by it", () => {
|
|
169
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG));
|
|
170
|
+
expect(sql).to.not.contain("as variation");
|
|
171
|
+
expect(sql).to.not.contain("impressions");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe("the --variation pairing", () => {
|
|
175
|
+
const VARIATIONS = inputFor(CONFIG, { variations: ["men", "women"] });
|
|
176
|
+
it("should pair every url with every variation", () => {
|
|
177
|
+
const { sql } = buildPrewarmQuery(VARIATIONS);
|
|
178
|
+
expect(sql).to.contain("cross join unnest(array['men', 'women']) as t (variation)");
|
|
179
|
+
expect(sql).to.contain("select urls.url, variation, urls.hits");
|
|
180
|
+
expect(sql).to.contain("order by urls.hits desc, variation");
|
|
181
|
+
});
|
|
182
|
+
it("should not pretend the traffic knows the variation", () => {
|
|
183
|
+
const { sql } = buildPrewarmQuery(VARIATIONS);
|
|
184
|
+
expect(sql).to.contain("device <> 'Robot'");
|
|
185
|
+
expect(sql).to.not.contain("lower(device)");
|
|
186
|
+
});
|
|
187
|
+
it("should quote a variation that carries a quote", () => {
|
|
188
|
+
expect(buildPrewarmQuery(inputFor(CONFIG, { variations: ["it's"] })).sql).to.contain("array['it''s']");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe("the --count report", () => {
|
|
192
|
+
it("should count the urls and the traffic they stand for", () => {
|
|
193
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { count: true }));
|
|
194
|
+
expect(sql).to.contain("select count(*) as urls, sum(hits) as impressions\nfrom urls");
|
|
195
|
+
expect(sql).to.not.contain("select url, count(*) as hits\nfrom");
|
|
196
|
+
});
|
|
197
|
+
it("should count the list every other option leaves", () => {
|
|
198
|
+
// Nothing about counting relaxes a cut: the number is the size of the
|
|
199
|
+
// list the same flags would produce, limit included.
|
|
200
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { count: true, limit: 5000, minHits: 5 }));
|
|
201
|
+
expect(sql).to.contain("urls as (\n select url, count(*) as hits\n from impressions\n group by url\n having count(*) >= 5\n order by hits desc\n limit 5000\n)");
|
|
202
|
+
});
|
|
203
|
+
it("should count a split list per variation, plus a total", () => {
|
|
204
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { count: true, devices: ["desktop", "mobile"] }));
|
|
205
|
+
// Over the pairs, so the total is what the pre-warmer would request —
|
|
206
|
+
// every url once per device class it was seen in.
|
|
207
|
+
expect(sql).to.contain("pairs as (");
|
|
208
|
+
expect(sql).to.contain("select coalesce(pairs.variation, 'total') as variation, count(*) as urls, sum(hits) as impressions");
|
|
209
|
+
expect(sql).to.contain("group by rollup(pairs.variation)");
|
|
210
|
+
// Qualified: an unqualified argument would resolve to the output alias of
|
|
211
|
+
// the same name, which Athena rejects in a grouping() expression.
|
|
212
|
+
expect(sql).to.contain("order by grouping(pairs.variation) desc, 2 desc");
|
|
213
|
+
});
|
|
214
|
+
it("should count custom variations as a product", () => {
|
|
215
|
+
// Every url carries every one of them, so a row per variation would be
|
|
216
|
+
// the same number written out three times.
|
|
217
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { count: true, variations: ["men", "women", "kids"] }));
|
|
218
|
+
expect(sql).to.contain("select count(*) as urls, 3 * count(*) as requests, sum(hits) as impressions");
|
|
219
|
+
expect(sql).to.not.contain("cross join unnest");
|
|
220
|
+
});
|
|
221
|
+
it("should leave the list itself alone", () => {
|
|
222
|
+
const options = { devices: ["desktop"], limit: 10, minHits: 2 };
|
|
223
|
+
const list = buildPrewarmQuery(inputFor(CONFIG, options)).sql;
|
|
224
|
+
const count = buildPrewarmQuery(inputFor(CONFIG, { ...options, count: true })).sql;
|
|
225
|
+
// Same relations, different projection: everything up to the final select
|
|
226
|
+
// is what the list is built from.
|
|
227
|
+
expect(count).to.contain(list.slice(0, list.indexOf("\n\nselect")));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
describe("the --match narrowing", () => {
|
|
231
|
+
it("should filter on the path and its query string", () => {
|
|
232
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { match: ["^/p/"] }));
|
|
233
|
+
// A version 1 pathname condition, whatever version the config runs under.
|
|
234
|
+
expect(sql).to.contain("as pathWithSearch");
|
|
235
|
+
expect(sql).to.contain("regexp_like(pathWithSearch, '^/p/')");
|
|
236
|
+
});
|
|
237
|
+
it("should reach a query parameter", () => {
|
|
238
|
+
expect(buildPrewarmQuery(inputFor(CONFIG, { match: ["[?&]size="] })).sql).to.contain("regexp_like(pathWithSearch, '[?&]size=')");
|
|
239
|
+
});
|
|
240
|
+
it("should keep the pages matching any of several patterns", () => {
|
|
241
|
+
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { match: ["^/p/", "^/c/"] }));
|
|
242
|
+
expect(sql).to.contain("(\n regexp_like(pathWithSearch, '^/p/')\n or regexp_like(pathWithSearch, '^/c/')\n )");
|
|
243
|
+
});
|
|
244
|
+
it("should carry inline flags over to Athena", () => {
|
|
245
|
+
// `new RegExp('(?i)…')` throws, so the flag is lifted out and put back.
|
|
246
|
+
expect(buildPrewarmQuery(inputFor(CONFIG, { match: ["(?i)^/P/"] })).sql).to.contain("regexp_like(pathWithSearch, '(?i)^/P/')");
|
|
247
|
+
});
|
|
248
|
+
it("should not project the subject when nothing matches on it", () => {
|
|
249
|
+
expect(buildPrewarmQuery(inputFor(CONFIG)).sql).to.not.contain("pathWithSearch");
|
|
250
|
+
});
|
|
251
|
+
it("should refuse a pattern the config excludes again", () => {
|
|
252
|
+
// The scope is disabled, so nothing under it is ever served by Speed Kit.
|
|
253
|
+
expect(() => buildPrewarmQuery(inputFor(WIDE_CONFIG, { match: ["^/checkout/thanks"] }))).to.throw(/--match is completely excluded again by disabledScopes/);
|
|
254
|
+
});
|
|
255
|
+
it("should restate a version 2 guard over the query string", () => {
|
|
256
|
+
// The guard is matched against the path, the pattern against path and
|
|
257
|
+
// query string; without the restatement the two are never compared.
|
|
258
|
+
const config = {
|
|
259
|
+
disabledSites: [{ pathname: [/^\/checkout$/] }],
|
|
260
|
+
enabledSites: [{ pathname: [/^\//] }],
|
|
261
|
+
ruleSetConfig: { version: 2 },
|
|
262
|
+
};
|
|
263
|
+
expect(() => buildPrewarmQuery(inputFor(config, { match: ["^/checkout\\?step="] }))).to.throw(/--match is completely excluded again by disabledSites/);
|
|
264
|
+
});
|
|
265
|
+
it("should not refuse a pattern the config only partly excludes", () => {
|
|
266
|
+
expect(() => buildPrewarmQuery(inputFor(WIDE_CONFIG, { match: ["^/"] }))).to.not.throw();
|
|
267
|
+
});
|
|
268
|
+
it("should not refuse a pattern an unanchored guard seems to cover", () => {
|
|
269
|
+
// /checkout/ says nothing about where in the path it sits, so it cannot
|
|
270
|
+
// be restated over the query string and takes part in no comparison.
|
|
271
|
+
const config = {
|
|
272
|
+
disabledSites: [{ pathname: [/checkout/] }],
|
|
273
|
+
enabledSites: [{ pathname: [/^\//] }],
|
|
274
|
+
ruleSetConfig: { version: 2 },
|
|
275
|
+
};
|
|
276
|
+
expect(() => buildPrewarmQuery(inputFor(config, { match: ["^/checkout/"] }))).to.not.throw();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe("buildParameterQuery", () => {
|
|
280
|
+
it("should keep a parameter the config does not strip", () => {
|
|
281
|
+
const { sql } = buildParameterQuery(inputFor(CONFIG));
|
|
282
|
+
expect(sql).to.contain("(key) -> not (lower(key) = 'gclid')");
|
|
283
|
+
});
|
|
284
|
+
it("should count only urls that carry a query string", () => {
|
|
285
|
+
expect(buildParameterQuery(inputFor(CONFIG)).sql).to.contain("cardinality(split(url, '?')) > 1");
|
|
286
|
+
});
|
|
287
|
+
it("should build the cache key from the parameters it keeps", () => {
|
|
288
|
+
const { sql } = buildParameterQuery(inputFor(CONFIG));
|
|
289
|
+
expect(sql).to.contain("path || '?' || array_join(pairs, '&') as cacheKey");
|
|
290
|
+
expect(sql).to.contain("count(distinct cacheKey) as cachedUrls");
|
|
291
|
+
});
|
|
292
|
+
it("should sort and deduplicate the parameters of a url", () => {
|
|
293
|
+
// Speed Kit sorts the query string, so `?b=1&a=1` and `?a=1&b=1` are one
|
|
294
|
+
// cache key — and one group, not two.
|
|
295
|
+
expect(buildParameterQuery(inputFor(CONFIG)).sql).to.contain("array_sort(\n array_distinct(");
|
|
296
|
+
});
|
|
297
|
+
it("should rank one row per parameter", () => {
|
|
298
|
+
const { sql } = buildParameterQuery(inputFor(CONFIG));
|
|
299
|
+
expect(sql).to.contain("group by parameter");
|
|
300
|
+
expect(sql).to.contain("count(distinct value) as distinctValues");
|
|
301
|
+
expect(sql).to.contain("count(distinct cacheKey) - count(distinct cacheKeyWithout) as potentialGain");
|
|
302
|
+
});
|
|
303
|
+
it("should rank whole sets when asked to", () => {
|
|
304
|
+
const { sql } = buildParameterQuery(inputFor(CONFIG, { bySet: true }));
|
|
305
|
+
expect(sql).to.contain("group by pairs");
|
|
306
|
+
expect(sql).to.contain("as paramKeys");
|
|
307
|
+
expect(sql).to.not.contain("group by parameter");
|
|
308
|
+
});
|
|
309
|
+
it("should leave out parameters below the impression threshold", () => {
|
|
310
|
+
expect(buildParameterQuery(inputFor(CONFIG, { minImpressions: 30 })).sql).to.contain("having count(*) >= 30");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { parseRuleSets } from "../../rules/index.js";
|
|
4
|
+
import { buildParameterQuery } from "../parameter-query.js";
|
|
5
|
+
import { buildPrewarmQuery } from "../prewarm-query.js";
|
|
6
|
+
/**
|
|
7
|
+
* The bugs that prompted the rework, each with the config that reported it.
|
|
8
|
+
*
|
|
9
|
+
* A golden query shows the whole output; these name the defect, so a
|
|
10
|
+
* regression fails with the ticket it belongs to rather than as an
|
|
11
|
+
* unexplained diff.
|
|
12
|
+
*/
|
|
13
|
+
function inputFor(config, hosts) {
|
|
14
|
+
return {
|
|
15
|
+
app: "shop",
|
|
16
|
+
days: 30,
|
|
17
|
+
hosts,
|
|
18
|
+
now: new Date("2026-09-04T09:30:00Z"),
|
|
19
|
+
ruleSets: parseRuleSets(config),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
describe("ED-277: a query from whitelisted pathnames", () => {
|
|
23
|
+
// The whitelist carried the page scope, and only its host was ever read, so
|
|
24
|
+
// the pathnames never reached the query.
|
|
25
|
+
const config = {
|
|
26
|
+
whitelist: [
|
|
27
|
+
{
|
|
28
|
+
contentType: ["navigate"],
|
|
29
|
+
host: ["www.hugendubel.de"],
|
|
30
|
+
pathname: [/\/de$/, /\/category/, /\.html/],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
const { sql } = buildPrewarmQuery(inputFor(config, ["www.hugendubel.de"]));
|
|
35
|
+
it("should filter on the whitelisted host", () => {
|
|
36
|
+
expect(sql).to.contain("lower(host) like 'www.hugendubel.de%'");
|
|
37
|
+
});
|
|
38
|
+
it("should filter on every whitelisted pathname", () => {
|
|
39
|
+
expect(sql).to.contain(String.raw `regexp_like(pathWithSearch, '/de$')`);
|
|
40
|
+
expect(sql).to.contain(String.raw `regexp_like(pathWithSearch, '/category')`);
|
|
41
|
+
expect(sql).to.contain(String.raw `regexp_like(pathWithSearch, '\.html')`);
|
|
42
|
+
});
|
|
43
|
+
it("should or the pathnames and and them with the host", () => {
|
|
44
|
+
const clause = sql.slice(sql.indexOf(" where lower(host)"));
|
|
45
|
+
expect(clause).to.match(/where lower\(host\) like[^\n]*\n {4}and \(/);
|
|
46
|
+
expect(clause).to.contain("\n or regexp_like(pathWithSearch");
|
|
47
|
+
});
|
|
48
|
+
it("should match the pathname the way the rule set version does", () => {
|
|
49
|
+
// No ruleSetConfig means version 1, where the runtime tests a pathname
|
|
50
|
+
// against `pathname + search` — not against the path alone.
|
|
51
|
+
expect(sql).to.not.contain("regexp_like(path,");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("ED-408: a pathname-scoped stripQueryParams rule", () => {
|
|
55
|
+
// `params: /.*/` was applied to every key of every page, so no key ever
|
|
56
|
+
// survived the filter and the analysis came back empty.
|
|
57
|
+
const config = {
|
|
58
|
+
ruleSetConfig: { version: 2 },
|
|
59
|
+
stripQueryParams: [
|
|
60
|
+
{ params: /.*/, rules: [{ pathname: "/hoteldetail/" }] },
|
|
61
|
+
{ params: ["_ga"] },
|
|
62
|
+
],
|
|
63
|
+
whitelist: [{ host: ["www.alltours.de"] }],
|
|
64
|
+
};
|
|
65
|
+
const { sql } = buildParameterQuery(inputFor(config, ["www.alltours.de"]));
|
|
66
|
+
it("should reject a key only on the scoped pathname", () => {
|
|
67
|
+
expect(sql).to.contain([
|
|
68
|
+
"(key) -> not (",
|
|
69
|
+
" lower(path) = '/hoteldetail/'",
|
|
70
|
+
" and regexp_like(key, '.*')",
|
|
71
|
+
" )",
|
|
72
|
+
].join("\n"));
|
|
73
|
+
});
|
|
74
|
+
it("should keep rejecting the unscoped parameters everywhere", () => {
|
|
75
|
+
expect(sql).to.contain("and not (lower(key) = '_ga')");
|
|
76
|
+
});
|
|
77
|
+
it("should scope the cache key the same way", () => {
|
|
78
|
+
// The cache key is built from the parameters that survive the very same
|
|
79
|
+
// filter, so the scope cannot apply to one and not the other.
|
|
80
|
+
expect(sql).to.contain("filter(pairs, (pair) -> contains(keptKeys, split(pair, '=')[1]))");
|
|
81
|
+
expect(sql).to.contain("path || '?' || array_join(pairs, '&') as cacheKey");
|
|
82
|
+
});
|
|
83
|
+
it("should never strip every parameter unconditionally", () => {
|
|
84
|
+
expect(sql).to.not.contain("(key) -> not regexp_like(key, '.*')");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { QueryBuilderService } from "./query-builder-service.js";
|
|
2
|
-
import { QueryBuilderFactoryContext,
|
|
2
|
+
import { QueryBuilderFactoryContext, QueryType } from "./query-builder-model.js";
|
|
3
3
|
import { UserCliConfig } from "../../helpers/cli-config.js";
|
|
4
4
|
export declare class QueryBuilderFactory {
|
|
5
5
|
private context;
|
|
6
6
|
private userConfig;
|
|
7
7
|
constructor(context: QueryBuilderFactoryContext, userConfig: UserCliConfig);
|
|
8
|
-
buildService(queryType:
|
|
8
|
+
buildService(queryType: QueryType): Promise<QueryBuilderService>;
|
|
9
9
|
private getIntegrationFiles;
|
|
10
10
|
private getSpeedKitConfig;
|
|
11
11
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { QueryBuilderService } from "./query-builder-service.js";
|
|
2
|
+
import { parseRuleSets } from "./rules/index.js";
|
|
2
3
|
import { IntegrationApiContext, IntegrationApiFactory, } from "../integration-api/index.js";
|
|
3
4
|
import { INTEGRATION_FILES } from "../../models/files.js";
|
|
4
5
|
import { CliServiceFactory } from "../cli/index.js";
|
|
@@ -18,21 +19,37 @@ export class QueryBuilderFactory {
|
|
|
18
19
|
const files = await this.getIntegrationFiles();
|
|
19
20
|
const customerConfig = files.getCustomerConfig().config;
|
|
20
21
|
const app = customerConfig.app;
|
|
21
|
-
const
|
|
22
|
+
const origins = customerConfig.origins ?? [];
|
|
22
23
|
const speedKitConfigFile = files.getByName(INTEGRATION_FILES.CONFIG.SPEED_KIT);
|
|
23
|
-
const speedKitConfig = await this.getSpeedKitConfig(app,
|
|
24
|
+
const speedKitConfig = await this.getSpeedKitConfig(app, origins[0], speedKitConfigFile);
|
|
24
25
|
const serviceContext = {
|
|
25
26
|
app,
|
|
26
|
-
|
|
27
|
+
bySet: this.context.bySet,
|
|
28
|
+
count: this.context.count,
|
|
29
|
+
days: this.context.days,
|
|
30
|
+
devices: this.context.devices,
|
|
31
|
+
excludeNotFound: this.context.excludeNotFound,
|
|
32
|
+
excludeNoindex: this.context.excludeNoindex,
|
|
33
|
+
excludeSuspicious: this.context.excludeSuspicious,
|
|
34
|
+
execute: this.context.execute,
|
|
35
|
+
explain: this.context.explain,
|
|
36
|
+
hardNavigationsOnly: this.context.hardNavigationsOnly,
|
|
37
|
+
hosts: toHosts(origins),
|
|
27
38
|
isWindows: this.context.isWindows,
|
|
39
|
+
limit: this.context.limit,
|
|
40
|
+
match: this.context.match,
|
|
41
|
+
minHits: this.context.minHits,
|
|
42
|
+
minImpressions: this.context.minImpressions,
|
|
43
|
+
queryType,
|
|
28
44
|
quiet: this.context.quiet,
|
|
29
|
-
|
|
45
|
+
variations: this.context.variations,
|
|
30
46
|
};
|
|
31
|
-
// The Athena service is only needed when the query is executed directly
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
// The Athena service is only needed when the query is executed directly —
|
|
48
|
+
// which `--count` is, a count query being of no use as text.
|
|
49
|
+
const athenaService = this.context.execute || this.context.count
|
|
50
|
+
? await new AthenaServiceFactory(app, this.context.workgroup).getService()
|
|
34
51
|
: undefined;
|
|
35
|
-
return new QueryBuilderService(serviceContext, speedKitConfig, cli, athenaService);
|
|
52
|
+
return new QueryBuilderService(serviceContext, parseRuleSets(speedKitConfig), cli, athenaService);
|
|
36
53
|
}
|
|
37
54
|
async getIntegrationFiles() {
|
|
38
55
|
const integrationApiContext = new IntegrationApiContext(this.context.customerPath, this.context.configName, ["config_SpeedKit"]);
|
|
@@ -49,3 +66,14 @@ export class QueryBuilderFactory {
|
|
|
49
66
|
return result.data;
|
|
50
67
|
}
|
|
51
68
|
}
|
|
69
|
+
/** The host of every origin, ignoring the ones that are not a URL. */
|
|
70
|
+
function toHosts(origins) {
|
|
71
|
+
const hosts = [];
|
|
72
|
+
for (const origin of origins) {
|
|
73
|
+
const result = safe(() => new URL(origin).host);
|
|
74
|
+
if (result.success === true) {
|
|
75
|
+
hosts.push(result.data);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return hosts;
|
|
79
|
+
}
|
|
@@ -1,24 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
queryType: QueryTpe;
|
|
6
|
-
app: string;
|
|
1
|
+
import { ParameterOptions, PrewarmOptions } from "./queries/query-model.js";
|
|
2
|
+
export declare enum QueryType {
|
|
3
|
+
parameter = "parameter",
|
|
4
|
+
prewarm = "prewarm"
|
|
7
5
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/** Options that narrow the rows a query looks at. */
|
|
7
|
+
export interface QueryFilterOptions {
|
|
8
|
+
/** Size of the window the query looks at, in days. */
|
|
9
|
+
readonly days?: number;
|
|
10
|
+
/** Drop page impressions of pages marked `noindex`. */
|
|
11
|
+
readonly excludeNoindex?: boolean;
|
|
12
|
+
/** Drop page impressions Speed Kit classified as suspicious. */
|
|
13
|
+
readonly excludeSuspicious?: boolean;
|
|
14
|
+
/** Keep hard navigations only, dropping soft (SPA) navigations. */
|
|
15
|
+
readonly hardNavigationsOnly?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface QueryBuilderContext extends ParameterOptions, PrewarmOptions, QueryFilterOptions {
|
|
18
|
+
readonly app: string;
|
|
19
|
+
readonly execute?: boolean;
|
|
20
|
+
/** Report every rule the query leaves out, reduces or approximates. */
|
|
21
|
+
readonly explain?: boolean;
|
|
22
|
+
/** Hosts of the app's origins. */
|
|
23
|
+
readonly hosts?: readonly string[];
|
|
11
24
|
readonly isWindows?: boolean;
|
|
25
|
+
readonly queryType: QueryType;
|
|
12
26
|
readonly quiet?: boolean;
|
|
13
|
-
readonly execute?: boolean;
|
|
14
|
-
}
|
|
15
|
-
export declare enum QueryTpe {
|
|
16
|
-
prewarm = "prewarm",
|
|
17
|
-
parameter = "parameter"
|
|
18
27
|
}
|
|
19
|
-
export interface
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
export interface QueryBuilderFactoryContext extends ParameterOptions, PrewarmOptions, QueryFilterOptions {
|
|
29
|
+
readonly configName: string;
|
|
30
|
+
readonly customerPath: string;
|
|
31
|
+
readonly execute?: boolean;
|
|
32
|
+
/** Report every rule the query leaves out, reduces or approximates. */
|
|
33
|
+
readonly explain?: boolean;
|
|
34
|
+
readonly isWindows?: boolean;
|
|
35
|
+
readonly quiet?: boolean;
|
|
36
|
+
/** Athena workgroup to execute in, when the query is executed at all. */
|
|
37
|
+
readonly workgroup?: string;
|
|
24
38
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export var
|
|
2
|
-
(function (
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
})(
|
|
1
|
+
export var QueryType;
|
|
2
|
+
(function (QueryType) {
|
|
3
|
+
QueryType["parameter"] = "parameter";
|
|
4
|
+
QueryType["prewarm"] = "prewarm";
|
|
5
|
+
})(QueryType || (QueryType = {}));
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { QueryBuilderContext
|
|
1
|
+
import { QueryBuilderContext } from "./query-builder-model.js";
|
|
2
|
+
import { ParsedRuleSets } from "./rules/index.js";
|
|
2
3
|
import { CliServiceInterface } from "../cli/index.js";
|
|
3
4
|
import { AthenaService } from "../athena/index.js";
|
|
4
5
|
export declare class QueryBuilderService {
|
|
5
6
|
private context;
|
|
6
|
-
private
|
|
7
|
+
private ruleSets;
|
|
7
8
|
private cli;
|
|
8
9
|
private athenaService?;
|
|
9
|
-
constructor(context: QueryBuilderContext,
|
|
10
|
+
constructor(context: QueryBuilderContext, ruleSets: ParsedRuleSets, cli: CliServiceInterface, athenaService?: AthenaService);
|
|
10
11
|
/** The app the query targets, as read from the customer config. */
|
|
11
12
|
get app(): string;
|
|
12
13
|
run(): Promise<void>;
|
|
@@ -16,13 +17,33 @@ export declare class QueryBuilderService {
|
|
|
16
17
|
* directly instead of exporting it (e.g. `auto-prewarm`).
|
|
17
18
|
*/
|
|
18
19
|
execute<T>(): Promise<T[]>;
|
|
20
|
+
/** The query as SQL. */
|
|
21
|
+
query(): string;
|
|
22
|
+
private build;
|
|
23
|
+
/**
|
|
24
|
+
* Points out where the query is not exactly what the config says — a rule
|
|
25
|
+
* left out, a rule the query can only approximate, a pattern Athena reads
|
|
26
|
+
* differently than the browser — plus what the config itself gets wrong.
|
|
27
|
+
*
|
|
28
|
+
* Reductions are silent unless asked for: a rule another rule already
|
|
29
|
+
* decides is not a divergence, it is arithmetic.
|
|
30
|
+
*/
|
|
31
|
+
private report;
|
|
32
|
+
/** The same accounting as {@link report}, as plain lines. */
|
|
33
|
+
private reportLines;
|
|
19
34
|
private runQuery;
|
|
35
|
+
/**
|
|
36
|
+
* Execute the count query and put its handful of rows on the terminal — a
|
|
37
|
+
* count is read, not filed, so nothing is written to disk.
|
|
38
|
+
*
|
|
39
|
+
* Under `--quiet` the rows go to stdout as tab separated values, so a count
|
|
40
|
+
* can be read by something other than a person.
|
|
41
|
+
*/
|
|
42
|
+
private executeAndReportCounts;
|
|
20
43
|
/**
|
|
21
44
|
* Execute the built query against Athena (Baqend native path, AWS fallback)
|
|
22
45
|
* and write the rows to a `<app>-<YYYY-MM-DD>-<queryType>.csv` file in the
|
|
23
46
|
* current working directory.
|
|
24
47
|
*/
|
|
25
48
|
private executeAndExport;
|
|
26
|
-
query(): string;
|
|
27
|
-
private buildQuery;
|
|
28
49
|
}
|