@speedkit/cli 4.26.0 → 4.27.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/CHANGELOG.md +10 -0
- package/README.md +186 -15
- package/dist/commands/auto-prewarm.d.ts +8 -1
- package/dist/commands/auto-prewarm.js +18 -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 +26 -0
- package/dist/commands/query/parameter.js +27 -0
- package/dist/commands/query/prewarm.d.ts +31 -0
- package/dist/commands/query/prewarm.js +31 -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 +73 -0
- package/dist/models/cli-parameters.js +151 -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.d.ts +2 -0
- package/dist/services/athena/athena-service.js +15 -4
- package/dist/services/athena/athena-service.spec.d.ts +1 -0
- package/dist/services/athena/athena-service.spec.js +74 -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 +12 -2
- package/dist/services/prewarm/auto-pre-warm-factory.js +20 -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 +35 -7
- package/dist/services/query-builder/query-builder-model.d.ts +31 -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 +26 -0
- package/dist/services/query-builder/query-command.js +35 -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 +350 -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,116 @@
|
|
|
1
|
+
export const GOLDEN_CONFIGS = [
|
|
2
|
+
{
|
|
3
|
+
config: {
|
|
4
|
+
blacklist: [
|
|
5
|
+
{
|
|
6
|
+
pathname: [
|
|
7
|
+
// Guards for pages `enabledSites` never allowed — the bulk of a
|
|
8
|
+
// real blacklist, and all reducible.
|
|
9
|
+
/^\/checkout/i,
|
|
10
|
+
/^\/cart/i,
|
|
11
|
+
/^\/account/i,
|
|
12
|
+
// These can hit an enabled page and have to survive.
|
|
13
|
+
/\.pdf/i,
|
|
14
|
+
/paypal/i,
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{ contentType: ["image", "script"], pathname: [/\/media\//i] },
|
|
18
|
+
{ host: ["tracking.example.com"] },
|
|
19
|
+
],
|
|
20
|
+
disabledScopes: ["/checkout", "/cart"],
|
|
21
|
+
disabledSites: [{ pathname: [/^\/search/i] }],
|
|
22
|
+
enabledSites: [
|
|
23
|
+
{
|
|
24
|
+
host: ["www.shop.de"],
|
|
25
|
+
pathname: [/^\/$/, /^\/p\//i, /^\/c\//i],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
ruleSetConfig: { version: 2 },
|
|
29
|
+
stripQueryParams: [{ params: ["gclid", /^utm[_-]/i] }],
|
|
30
|
+
whitelist: [{ contentType: ["navigate"], host: ["www.shop.de"] }],
|
|
31
|
+
},
|
|
32
|
+
describes: "the recommended shape: version 2, enabledSites, and a blacklist most of which cannot fire",
|
|
33
|
+
hosts: ["www.shop.de"],
|
|
34
|
+
name: "enabled-sites-v2",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
config: {
|
|
38
|
+
blacklist: [{ pathname: ["/login", "/warenkorb"] }],
|
|
39
|
+
disabledSites: [{ pathname: [/\/kasse/i] }],
|
|
40
|
+
// No ruleSetConfig: version 1, where a string is a prefix and a
|
|
41
|
+
// pathname pattern is matched against the query string as well.
|
|
42
|
+
stripQueryParams: ["utm", /session$/i],
|
|
43
|
+
whitelist: [{ host: ["www.shop.de", "m.shop.de"] }],
|
|
44
|
+
},
|
|
45
|
+
describes: "a version 1 config without enabledSites, where every page is enabled unless disabled",
|
|
46
|
+
hosts: ["www.shop.de", "m.shop.de"],
|
|
47
|
+
name: "disabled-sites-v1",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
config: {
|
|
51
|
+
enabledSites: [
|
|
52
|
+
{ host: ["www.shop.de"], pathname: [/^\/de\//i] },
|
|
53
|
+
{ host: ["www.shop.fr"], pathname: [/^\/fr\//i] },
|
|
54
|
+
],
|
|
55
|
+
ruleSetConfig: { version: 2 },
|
|
56
|
+
whitelist: [{ host: ["www.shop.de", "www.shop.fr"] }],
|
|
57
|
+
},
|
|
58
|
+
describes: "one market per rule, which only works if host and pathname stay together",
|
|
59
|
+
hosts: ["www.shop.de", "www.shop.fr"],
|
|
60
|
+
name: "multi-market",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
config: {
|
|
64
|
+
enabledSites: [{ pathname: [/^\/p\//i, /^\/c\//i] }],
|
|
65
|
+
ruleSetConfig: { version: 2 },
|
|
66
|
+
stripQueryParams: [
|
|
67
|
+
{ params: ["gclid"] },
|
|
68
|
+
// Only stripped on product pages.
|
|
69
|
+
{ enabledSites: [{ pathname: [/^\/p\//i] }], params: ["size"] },
|
|
70
|
+
// Only stripped on images, which a page impression never is.
|
|
71
|
+
{ params: ["width"], rules: [{ contentType: ["image"] }] },
|
|
72
|
+
],
|
|
73
|
+
whitelist: [{ host: ["www.shop.de"] }],
|
|
74
|
+
},
|
|
75
|
+
describes: "parameters stripped only on certain pages or resources",
|
|
76
|
+
hosts: ["www.shop.de"],
|
|
77
|
+
name: "scoped-strip-params",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
config: {
|
|
81
|
+
blacklist: [
|
|
82
|
+
// A real shape: the hosts are only blacklisted for browsers without
|
|
83
|
+
// WebP, and the evaluated config carries the empty branch. Read as
|
|
84
|
+
// "any host" this rule would exclude every page.
|
|
85
|
+
{ host: [] },
|
|
86
|
+
{ pathname: [/^\/checkout/i] },
|
|
87
|
+
],
|
|
88
|
+
enabledSites: [{ pathname: [/^\/$/, /^\/p\//i] }],
|
|
89
|
+
ruleSetConfig: { version: 2 },
|
|
90
|
+
whitelist: [{ host: ["www.shop.de"] }],
|
|
91
|
+
},
|
|
92
|
+
describes: "a condition that is an empty list, which matches nothing",
|
|
93
|
+
hosts: ["www.shop.de"],
|
|
94
|
+
name: "empty-condition",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
config: {
|
|
98
|
+
blacklist: [
|
|
99
|
+
{ contentType: ["fetch"] },
|
|
100
|
+
{ url: ["www.googletagmanager.com/gtm.js"] },
|
|
101
|
+
// A word boundary has no automaton, so no reduction may touch it.
|
|
102
|
+
{ pathname: [/\bprint\b/i] },
|
|
103
|
+
],
|
|
104
|
+
disabledSites: [
|
|
105
|
+
{ cookie: /loggedIn=true/ },
|
|
106
|
+
{ mobile: true, pathname: [/^\/app\//i] },
|
|
107
|
+
],
|
|
108
|
+
enabledSites: [{ pathname: [/^\//i] }],
|
|
109
|
+
ruleSetConfig: { version: 2 },
|
|
110
|
+
whitelist: [],
|
|
111
|
+
},
|
|
112
|
+
describes: "rules the query cannot express literally: a cookie, a device, a pattern with no automaton",
|
|
113
|
+
hosts: ["www.shop.de"],
|
|
114
|
+
name: "unexpressible",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { expect } from "chai";
|
|
5
|
+
import { describe, it } from "mocha";
|
|
6
|
+
import { parseRuleSets } from "../../rules/index.js";
|
|
7
|
+
import { buildParameterQuery } from "../parameter-query.js";
|
|
8
|
+
import { buildPopsQuery } from "../pops-query.js";
|
|
9
|
+
import { buildPrewarmQuery } from "../prewarm-query.js";
|
|
10
|
+
import { GOLDEN_CONFIGS } from "./golden-configs.js";
|
|
11
|
+
/**
|
|
12
|
+
* The queries these configs produce, in full, checked in beside them.
|
|
13
|
+
*
|
|
14
|
+
* A unit test states one property; a golden query states all of them at once,
|
|
15
|
+
* which is what a reviewer needs for a change that rewrites SQL. Run with
|
|
16
|
+
* `UPDATE_GOLDEN=1 npm test` to write the files, then read the diff — an
|
|
17
|
+
* unexplained line there is a bug, and the diff is the review.
|
|
18
|
+
*/
|
|
19
|
+
const DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), "golden");
|
|
20
|
+
/** A fixed day, so a window never makes a golden query go stale. */
|
|
21
|
+
const NOW = new Date("2026-09-04T09:30:00Z");
|
|
22
|
+
const APP = "shop-de";
|
|
23
|
+
function goldenText(query) {
|
|
24
|
+
const notes = [
|
|
25
|
+
...query.findings.map(({ reason, source }) => `finding: ${source} ${reason}`),
|
|
26
|
+
...query.dropped.map(({ reason, source }) => `dropped: ${source} — ${reason}`),
|
|
27
|
+
...query.approximated.map(({ reason, source }) => `approximated: ${source} — ${reason}`),
|
|
28
|
+
...query.reduced.map(({ reason, source }) => `reduced: ${source} — ${reason}`),
|
|
29
|
+
...query.warnings.map((warning) => `warning: ${warning}`),
|
|
30
|
+
];
|
|
31
|
+
return [
|
|
32
|
+
query.sql.trim(),
|
|
33
|
+
"",
|
|
34
|
+
"-- accounting",
|
|
35
|
+
...(notes.length > 0
|
|
36
|
+
? notes.map((note) => `-- ${note}`)
|
|
37
|
+
: ["-- nothing left out, reduced or approximated"]),
|
|
38
|
+
"",
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
/** Compares against the checked-in file, or writes it when asked to. */
|
|
42
|
+
function expectGolden(name, text) {
|
|
43
|
+
const file = join(DIRECTORY, `${name}.sql`);
|
|
44
|
+
if (process.env.UPDATE_GOLDEN) {
|
|
45
|
+
mkdirSync(DIRECTORY, { recursive: true });
|
|
46
|
+
writeFileSync(file, text, "utf8");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let expected;
|
|
50
|
+
try {
|
|
51
|
+
expected = readFileSync(file, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`No golden query at ${file}. Run UPDATE_GOLDEN=1 npm test to write it.`);
|
|
55
|
+
}
|
|
56
|
+
expect(text).to.equal(expected);
|
|
57
|
+
}
|
|
58
|
+
/** The prewarm query for one of the golden configs, with options on top. */
|
|
59
|
+
function prewarmQueryFor(name, options) {
|
|
60
|
+
const { config, hosts } = GOLDEN_CONFIGS.find((candidate) => candidate.name === name);
|
|
61
|
+
return buildPrewarmQuery({
|
|
62
|
+
app: APP,
|
|
63
|
+
days: 10,
|
|
64
|
+
hosts,
|
|
65
|
+
now: NOW,
|
|
66
|
+
ruleSets: parseRuleSets(config),
|
|
67
|
+
...options,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
describe("golden queries", () => {
|
|
71
|
+
for (const { config, describes, hosts, name } of GOLDEN_CONFIGS) {
|
|
72
|
+
describe(`${name} — ${describes}`, () => {
|
|
73
|
+
const input = {
|
|
74
|
+
app: APP,
|
|
75
|
+
days: 10,
|
|
76
|
+
hosts,
|
|
77
|
+
now: NOW,
|
|
78
|
+
ruleSets: parseRuleSets(config),
|
|
79
|
+
};
|
|
80
|
+
it("should build the same prewarm query", () => {
|
|
81
|
+
expectGolden(`${name}.prewarm`, goldenText(buildPrewarmQuery(input)));
|
|
82
|
+
});
|
|
83
|
+
it("should build the same parameter query", () => {
|
|
84
|
+
expectGolden(`${name}.parameter`, goldenText(buildParameterQuery({ ...input, days: 30 })));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
describe("match — a prewarm query narrowed by --match", () => {
|
|
89
|
+
it("should build the same prewarm query", () => {
|
|
90
|
+
expectGolden("enabled-sites-v2.match.prewarm", goldenText(prewarmQueryFor("enabled-sites-v2", {
|
|
91
|
+
match: ["^/p/", "(?i)^/c/[^?]*sale", "[?&]size="],
|
|
92
|
+
})));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("device — a prewarm list split by device class", () => {
|
|
96
|
+
it("should carry the variation and still judge urls", () => {
|
|
97
|
+
// Every url-level cut at once: the limit, the threshold and the 404 rule
|
|
98
|
+
// all belong to the inner `urls` relation, never to the pairs.
|
|
99
|
+
expectGolden("enabled-sites-v2.device.prewarm", goldenText(prewarmQueryFor("enabled-sites-v2", {
|
|
100
|
+
devices: ["desktop", "mobile"],
|
|
101
|
+
excludeNotFound: true,
|
|
102
|
+
limit: 5000,
|
|
103
|
+
minHits: 2,
|
|
104
|
+
})));
|
|
105
|
+
});
|
|
106
|
+
it("should split a list whose urls are reduced to a cache key", () => {
|
|
107
|
+
expectGolden("scoped-strip-params.device.prewarm", goldenText(prewarmQueryFor("scoped-strip-params", {
|
|
108
|
+
devices: ["desktop", "mobile", "tablet"],
|
|
109
|
+
})));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("variation — a prewarm list paired with custom variations", () => {
|
|
113
|
+
it("should cross every url with every variation", () => {
|
|
114
|
+
expectGolden("enabled-sites-v2.variation.prewarm", goldenText(prewarmQueryFor("enabled-sites-v2", {
|
|
115
|
+
variations: ["desktop-pdp-a", "mobile-pdp-a", "break"],
|
|
116
|
+
})));
|
|
117
|
+
});
|
|
118
|
+
it("should pair urls that were reduced to a cache key", () => {
|
|
119
|
+
expectGolden("scoped-strip-params.variation.prewarm", goldenText(prewarmQueryFor("scoped-strip-params", {
|
|
120
|
+
limit: 5000,
|
|
121
|
+
variations: ["men", "women"],
|
|
122
|
+
})));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe("count — how large the list is, instead of what is in it", () => {
|
|
126
|
+
it("should count the list every other flag leaves", () => {
|
|
127
|
+
expectGolden("enabled-sites-v2.count.prewarm", goldenText(prewarmQueryFor("enabled-sites-v2", {
|
|
128
|
+
count: true,
|
|
129
|
+
excludeNotFound: true,
|
|
130
|
+
limit: 5000,
|
|
131
|
+
minHits: 2,
|
|
132
|
+
})));
|
|
133
|
+
});
|
|
134
|
+
it("should count a split list per device class, plus a total", () => {
|
|
135
|
+
expectGolden("enabled-sites-v2.device.count.prewarm", goldenText(prewarmQueryFor("enabled-sites-v2", {
|
|
136
|
+
count: true,
|
|
137
|
+
devices: ["desktop", "mobile"],
|
|
138
|
+
})));
|
|
139
|
+
});
|
|
140
|
+
it("should count custom variations as a product, not as rows", () => {
|
|
141
|
+
expectGolden("enabled-sites-v2.variation.count.prewarm", goldenText(prewarmQueryFor("enabled-sites-v2", {
|
|
142
|
+
count: true,
|
|
143
|
+
variations: ["desktop-pdp-a", "mobile-pdp-a", "break"],
|
|
144
|
+
})));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe("pops — the same query for every app", () => {
|
|
148
|
+
it("should build the same pops query", () => {
|
|
149
|
+
const { parameters, sql } = buildPopsQuery({ app: APP, now: NOW });
|
|
150
|
+
expectGolden("pops", [sql.trim(), "", `-- parameters: ${parameters.join(", ")}`, ""].join("\n"));
|
|
151
|
+
});
|
|
152
|
+
it("should build the same pops query from rum", () => {
|
|
153
|
+
const { parameters, sql } = buildPopsQuery({
|
|
154
|
+
app: APP,
|
|
155
|
+
now: NOW,
|
|
156
|
+
useRum: true,
|
|
157
|
+
});
|
|
158
|
+
expectGolden("pops.rum", [sql.trim(), "", `-- parameters: ${parameters.join(", ")}`, ""].join("\n"));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { buildPopsQuery } from "../pops-query.js";
|
|
4
|
+
import { sinceDate, today } from "../query-model.js";
|
|
5
|
+
describe("buildPopsQuery", () => {
|
|
6
|
+
it("should read the request log, not the page impressions", () => {
|
|
7
|
+
const { sql } = buildPopsQuery({ app: "shop-de" });
|
|
8
|
+
expect(sql).to.contain("from fastly.logs");
|
|
9
|
+
expect(sql).to.not.contain("rum.pi");
|
|
10
|
+
});
|
|
11
|
+
it("should pass the window and the app as parameters, in order", () => {
|
|
12
|
+
const { parameters, sql } = buildPopsQuery({ app: "shop-de" });
|
|
13
|
+
expect(sql).to.contain("where date between ? and ?");
|
|
14
|
+
expect(sql).to.contain("and app = ?");
|
|
15
|
+
expect(parameters).to.deep.equal([sinceDate(7), today(), "shop-de"]);
|
|
16
|
+
});
|
|
17
|
+
it("should look back a week by default", () => {
|
|
18
|
+
const [start, end] = buildPopsQuery({ app: "shop-de" }).parameters;
|
|
19
|
+
const days = (Date.parse(String(end)) - Date.parse(String(start))) / 86_400_000;
|
|
20
|
+
expect(days).to.equal(7);
|
|
21
|
+
});
|
|
22
|
+
it("should take the window from the caller", () => {
|
|
23
|
+
const { parameters } = buildPopsQuery({ app: "shop-de", days: 30 });
|
|
24
|
+
expect(parameters[0]).to.equal(sinceDate(30));
|
|
25
|
+
});
|
|
26
|
+
it("should count Speed Kit's own asset requests by default", () => {
|
|
27
|
+
const { sql } = buildPopsQuery({ app: "shop-de" });
|
|
28
|
+
expect(sql).to.contain("and isassetrequest");
|
|
29
|
+
expect(sql).to.contain("and isnavigaterequest");
|
|
30
|
+
expect(sql).to.not.contain("ispibeacon");
|
|
31
|
+
});
|
|
32
|
+
it("should count beacons instead when there is no accelerated traffic", () => {
|
|
33
|
+
const { sql } = buildPopsQuery({ app: "shop-de", useRum: true });
|
|
34
|
+
expect(sql).to.contain("and ispibeacon");
|
|
35
|
+
expect(sql).to.not.contain("isassetrequest");
|
|
36
|
+
});
|
|
37
|
+
it("should exclude bots and unknown origins", () => {
|
|
38
|
+
const { sql } = buildPopsQuery({ app: "shop-de" });
|
|
39
|
+
expect(sql).to.contain("and client.bot is null");
|
|
40
|
+
expect(sql).to.contain("and origin not in ('Unknown', '')");
|
|
41
|
+
});
|
|
42
|
+
it("should keep the locale prefix of an origin", () => {
|
|
43
|
+
const { sql } = buildPopsQuery({ app: "shop-de" });
|
|
44
|
+
expect(sql).to.contain("regexp_extract(url_extract_path(asseturl)");
|
|
45
|
+
expect(sql).to.contain(String.raw `(?i)((/\w+)?/[a-z]{2}([-/_][a-z]{2})*/)`);
|
|
46
|
+
});
|
|
47
|
+
it("should aggregate per region, pop and origin", () => {
|
|
48
|
+
const { sql } = buildPopsQuery({ app: "shop-de" });
|
|
49
|
+
expect(sql).to.contain("coalesce(originWithPrefix, origin) as origin");
|
|
50
|
+
expect(sql).to.contain("count() as hits");
|
|
51
|
+
expect(sql).to.contain("group by grouping sets (");
|
|
52
|
+
expect(sql).to.contain("having originWithPrefix is null or originWithPrefix <> 'Unknown'");
|
|
53
|
+
});
|
|
54
|
+
it("should project only what the aggregation reads", () => {
|
|
55
|
+
// `select *` would carry every column of the log through the CTE.
|
|
56
|
+
expect(buildPopsQuery({ app: "shop-de" }).sql).to.not.contain("select *");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|