@beesolve/analytics-processor 0.4.0 → 0.5.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/DOCS.md +23 -3
- package/dist/cdk.js +46 -0
- package/dist/dailyRollup/dailyRollup.mjs.map +2 -2
- package/dist/hourly/hourly.mjs +3 -3
- package/dist/hourly/hourly.mjs.map +3 -3
- package/dist/index.d.ts +233 -3
- package/dist/index.js +509 -40
- package/dist/monthlyRollup/monthlyRollup.mjs.map +2 -2
- package/dist/sitemap/bowser-UJEXXM6S.mjs +2 -0
- package/dist/sitemap/bowser-UJEXXM6S.mjs.map +7 -0
- package/dist/sitemap/chunk-3UGRDNDZ.mjs +6 -0
- package/dist/sitemap/chunk-3UGRDNDZ.mjs.map +7 -0
- package/dist/sitemap/chunk-BH7ZEBAB.mjs +2 -0
- package/dist/sitemap/chunk-BH7ZEBAB.mjs.map +7 -0
- package/dist/sitemap/chunk-CFZ5JWD3.mjs +2 -0
- package/dist/sitemap/chunk-CFZ5JWD3.mjs.map +7 -0
- package/dist/sitemap/chunk-KRS5YV2Z.mjs +2 -0
- package/dist/sitemap/chunk-KRS5YV2Z.mjs.map +7 -0
- package/dist/sitemap/chunk-LB33VBGK.mjs +2 -0
- package/dist/sitemap/chunk-LB33VBGK.mjs.map +7 -0
- package/dist/sitemap/chunk-OC3UN3OS.mjs +2 -0
- package/dist/sitemap/chunk-OC3UN3OS.mjs.map +7 -0
- package/dist/sitemap/chunk-PD2XGJQD.mjs +10 -0
- package/dist/sitemap/chunk-PD2XGJQD.mjs.map +7 -0
- package/dist/sitemap/chunk-QPD5NJHJ.mjs +2 -0
- package/dist/sitemap/chunk-QPD5NJHJ.mjs.map +7 -0
- package/dist/sitemap/chunk-REMPUQ6B.mjs +19 -0
- package/dist/sitemap/chunk-REMPUQ6B.mjs.map +7 -0
- package/dist/sitemap/chunk-SNCZ4GCT.mjs +2 -0
- package/dist/sitemap/chunk-SNCZ4GCT.mjs.map +7 -0
- package/dist/sitemap/chunk-TZ6TJSKE.mjs +10 -0
- package/dist/sitemap/chunk-TZ6TJSKE.mjs.map +7 -0
- package/dist/sitemap/chunk-YMGL4RSC.mjs +2 -0
- package/dist/sitemap/chunk-YMGL4RSC.mjs.map +7 -0
- package/dist/sitemap/chunk-YY2HYMK3.mjs +4 -0
- package/dist/sitemap/chunk-YY2HYMK3.mjs.map +7 -0
- package/dist/sitemap/client-WGCJRHWA.mjs +2 -0
- package/dist/sitemap/client-WGCJRHWA.mjs.map +7 -0
- package/dist/sitemap/dist-es-3FAKVHIH.mjs +2 -0
- package/dist/sitemap/dist-es-3FAKVHIH.mjs.map +7 -0
- package/dist/sitemap/dist-es-A36NQ5CO.mjs +2 -0
- package/dist/sitemap/dist-es-A36NQ5CO.mjs.map +7 -0
- package/dist/sitemap/dist-es-F7GXZJIC.mjs +3 -0
- package/dist/sitemap/dist-es-F7GXZJIC.mjs.map +7 -0
- package/dist/sitemap/dist-es-GRXF7RTK.mjs +2 -0
- package/dist/sitemap/dist-es-GRXF7RTK.mjs.map +7 -0
- package/dist/sitemap/dist-es-OKUHAI6O.mjs +2 -0
- package/dist/sitemap/dist-es-OKUHAI6O.mjs.map +7 -0
- package/dist/sitemap/dist-es-P7BXOYHO.mjs +2 -0
- package/dist/sitemap/dist-es-P7BXOYHO.mjs.map +7 -0
- package/dist/sitemap/dist-es-SQ53M5JX.mjs +7 -0
- package/dist/sitemap/dist-es-SQ53M5JX.mjs.map +7 -0
- package/dist/sitemap/dist-es-TCGA3UGX.mjs +2 -0
- package/dist/sitemap/dist-es-TCGA3UGX.mjs.map +7 -0
- package/dist/sitemap/dist-es-UXKF5GQY.mjs +3 -0
- package/dist/sitemap/dist-es-UXKF5GQY.mjs.map +7 -0
- package/dist/sitemap/dist-es-YJ4PQZC7.mjs +2 -0
- package/dist/sitemap/dist-es-YJ4PQZC7.mjs.map +7 -0
- package/dist/sitemap/event-streams-6V3Q6GHC.mjs +2 -0
- package/dist/sitemap/event-streams-6V3Q6GHC.mjs.map +7 -0
- package/dist/sitemap/loadCognitoIdentity-RIENBHOX.mjs +2 -0
- package/dist/sitemap/loadCognitoIdentity-RIENBHOX.mjs.map +7 -0
- package/dist/sitemap/loadSso-DTQ4R4CV.mjs +2 -0
- package/dist/sitemap/loadSso-DTQ4R4CV.mjs.map +7 -0
- package/dist/sitemap/loadSts-GZDTRQPT.mjs +2 -0
- package/dist/sitemap/loadSts-GZDTRQPT.mjs.map +7 -0
- package/dist/sitemap/signin-6D536OAO.mjs +2 -0
- package/dist/sitemap/signin-6D536OAO.mjs.map +7 -0
- package/dist/sitemap/sitemap.mjs +3 -0
- package/dist/sitemap/sitemap.mjs.map +7 -0
- package/dist/sitemap/sso-oidc-T5CK3NX3.mjs +2 -0
- package/dist/sitemap/sso-oidc-T5CK3NX3.mjs.map +7 -0
- package/dist/sitemap/sts-62ECLMPL.mjs +2 -0
- package/dist/sitemap/sts-62ECLMPL.mjs.map +7 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -328,7 +328,7 @@ async function waitForQuery({
|
|
|
328
328
|
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
329
329
|
}
|
|
330
330
|
}
|
|
331
|
-
async function
|
|
331
|
+
async function fetchRawRows({
|
|
332
332
|
athena,
|
|
333
333
|
queryExecutionId
|
|
334
334
|
}) {
|
|
@@ -340,25 +340,20 @@ async function fetchRows({
|
|
|
340
340
|
const resultRows = results.ResultSet?.Rows ?? [];
|
|
341
341
|
const dataRows = isFirstPage ? resultRows.slice(1) : resultRows;
|
|
342
342
|
for (const dataRow of dataRows) {
|
|
343
|
-
|
|
344
|
-
rows.push(parseRow(cells));
|
|
343
|
+
rows.push((dataRow.Data ?? []).map((datum) => datum.VarCharValue));
|
|
345
344
|
}
|
|
346
345
|
nextToken = results.NextToken;
|
|
347
346
|
isFirstPage = false;
|
|
348
347
|
} while (nextToken != null);
|
|
349
348
|
return rows;
|
|
350
349
|
}
|
|
351
|
-
async function
|
|
350
|
+
async function startAndFetchRows({
|
|
352
351
|
athena,
|
|
353
352
|
database,
|
|
354
|
-
table,
|
|
355
353
|
workgroup,
|
|
356
354
|
outputLocation,
|
|
357
|
-
|
|
358
|
-
hourKey,
|
|
359
|
-
topPaths
|
|
355
|
+
queryString
|
|
360
356
|
}) {
|
|
361
|
-
const queryString = buildHourlyQuery({ table, source, hourKey, topPaths });
|
|
362
357
|
const started = await athena.send(new StartQueryExecutionCommand({
|
|
363
358
|
QueryString: queryString,
|
|
364
359
|
QueryExecutionContext: { Database: database },
|
|
@@ -370,7 +365,27 @@ async function runHourlyAthenaQuery({
|
|
|
370
365
|
throw new Error("Athena StartQueryExecution returned no QueryExecutionId");
|
|
371
366
|
}
|
|
372
367
|
await waitForQuery({ athena, queryExecutionId });
|
|
373
|
-
|
|
368
|
+
return fetchRawRows({ athena, queryExecutionId });
|
|
369
|
+
}
|
|
370
|
+
async function runHourlyAthenaQuery({
|
|
371
|
+
athena,
|
|
372
|
+
database,
|
|
373
|
+
table,
|
|
374
|
+
workgroup,
|
|
375
|
+
outputLocation,
|
|
376
|
+
source,
|
|
377
|
+
hourKey,
|
|
378
|
+
topPaths
|
|
379
|
+
}) {
|
|
380
|
+
const queryString = buildHourlyQuery({ table, source, hourKey, topPaths });
|
|
381
|
+
const rawRows = await startAndFetchRows({
|
|
382
|
+
athena,
|
|
383
|
+
database,
|
|
384
|
+
workgroup,
|
|
385
|
+
outputLocation,
|
|
386
|
+
queryString
|
|
387
|
+
});
|
|
388
|
+
const rows = rawRows.map((cells) => parseRow(cells));
|
|
374
389
|
return assembleResult({ rows, topPaths });
|
|
375
390
|
}
|
|
376
391
|
// packages/analytics-processor/src/breakdowns.ts
|
|
@@ -489,7 +504,11 @@ var configSchema = v3.object({
|
|
|
489
504
|
pk: v3.literal("system#config"),
|
|
490
505
|
sk: v3.literal("analytics"),
|
|
491
506
|
startKey: v3.string(),
|
|
492
|
-
startedAt: v3.string()
|
|
507
|
+
startedAt: v3.string(),
|
|
508
|
+
sitemapUrl: v3.optional(v3.string())
|
|
509
|
+
});
|
|
510
|
+
var sitemapConfigSchema = v3.looseObject({
|
|
511
|
+
sitemapUrl: v3.optional(v3.string())
|
|
493
512
|
});
|
|
494
513
|
var configKey = { pk: "system#config", sk: "analytics" };
|
|
495
514
|
function isConditionalCheckFailure(error) {
|
|
@@ -510,6 +529,23 @@ class Config {
|
|
|
510
529
|
return null;
|
|
511
530
|
return v3.parse(configSchema, item);
|
|
512
531
|
};
|
|
532
|
+
getSitemapConfig = async () => {
|
|
533
|
+
const { Item: item } = await this.props.dynamo.send(new GetCommand2({ TableName: this.props.tableName, Key: configKey }));
|
|
534
|
+
if (item == null)
|
|
535
|
+
return {};
|
|
536
|
+
return v3.parse(sitemapConfigSchema, item);
|
|
537
|
+
};
|
|
538
|
+
setSitemapUrl = async ({
|
|
539
|
+
sitemapUrl
|
|
540
|
+
}) => {
|
|
541
|
+
await this.props.dynamo.send(new UpdateCommand2({
|
|
542
|
+
TableName: this.props.tableName,
|
|
543
|
+
Key: configKey,
|
|
544
|
+
UpdateExpression: "SET #sitemapUrl = :url",
|
|
545
|
+
ExpressionAttributeNames: { "#sitemapUrl": "sitemapUrl" },
|
|
546
|
+
ExpressionAttributeValues: { ":url": sitemapUrl }
|
|
547
|
+
}));
|
|
548
|
+
};
|
|
513
549
|
ensureStart = async (props) => {
|
|
514
550
|
try {
|
|
515
551
|
await this.props.dynamo.send(new UpdateCommand2({
|
|
@@ -1031,6 +1067,423 @@ function pad2(value) {
|
|
|
1031
1067
|
function pad4(value) {
|
|
1032
1068
|
return value.toString().padStart(4, "0");
|
|
1033
1069
|
}
|
|
1070
|
+
// packages/analytics-processor/src/sitemap.ts
|
|
1071
|
+
import { UpdateCommand as UpdateCommand3 } from "@aws-sdk/lib-dynamodb";
|
|
1072
|
+
import { queryAll as queryAll2 } from "@beesolve/dynamo-helpers";
|
|
1073
|
+
import * as v4 from "valibot";
|
|
1074
|
+
function canonicalPath(path) {
|
|
1075
|
+
if (path.length > 1 && path.endsWith("/")) {
|
|
1076
|
+
return path.slice(0, -1);
|
|
1077
|
+
}
|
|
1078
|
+
return path;
|
|
1079
|
+
}
|
|
1080
|
+
function normalizeSitemapLoc(loc) {
|
|
1081
|
+
let parsed;
|
|
1082
|
+
try {
|
|
1083
|
+
parsed = new URL(loc);
|
|
1084
|
+
} catch {
|
|
1085
|
+
throw new Error(`Unparseable sitemap <loc>: ${loc}`);
|
|
1086
|
+
}
|
|
1087
|
+
return canonicalPath(parsed.pathname);
|
|
1088
|
+
}
|
|
1089
|
+
function parseSitemap(xml) {
|
|
1090
|
+
if (sitemapIndexPattern.test(xml)) {
|
|
1091
|
+
throw new Error("Sitemap index documents are not supported");
|
|
1092
|
+
}
|
|
1093
|
+
const paths = [];
|
|
1094
|
+
const seen = new Set;
|
|
1095
|
+
for (const match of xml.matchAll(locPattern)) {
|
|
1096
|
+
const raw = match[1];
|
|
1097
|
+
if (raw == null)
|
|
1098
|
+
continue;
|
|
1099
|
+
const loc = unescapeXml(raw.trim());
|
|
1100
|
+
if (loc === "")
|
|
1101
|
+
continue;
|
|
1102
|
+
const path = normalizeSitemapLoc(loc);
|
|
1103
|
+
if (seen.has(path))
|
|
1104
|
+
continue;
|
|
1105
|
+
seen.add(path);
|
|
1106
|
+
paths.push(path);
|
|
1107
|
+
}
|
|
1108
|
+
return paths;
|
|
1109
|
+
}
|
|
1110
|
+
var sitemapUserAgent = "@beesolve/analytics-processor sitemap fetcher (+https://github.com/BeeSolve/packages)";
|
|
1111
|
+
async function fetchSitemapPaths(url) {
|
|
1112
|
+
const res = await fetch(url, { headers: { "user-agent": sitemapUserAgent } });
|
|
1113
|
+
if (!res.ok) {
|
|
1114
|
+
throw new Error(`Failed to fetch sitemap ${url}: ${res.status} ${res.statusText}`);
|
|
1115
|
+
}
|
|
1116
|
+
const text = await res.text();
|
|
1117
|
+
return parseSitemap(text);
|
|
1118
|
+
}
|
|
1119
|
+
function hourKeyParts2(hourKey) {
|
|
1120
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})-(\d{2})$/.exec(hourKey);
|
|
1121
|
+
if (match == null) {
|
|
1122
|
+
throw new Error(`Invalid hourKey: ${hourKey}`);
|
|
1123
|
+
}
|
|
1124
|
+
const [, year, month, day, hour] = match;
|
|
1125
|
+
if (year == null || month == null || day == null || hour == null) {
|
|
1126
|
+
throw new Error(`Invalid hourKey: ${hourKey}`);
|
|
1127
|
+
}
|
|
1128
|
+
return { year, month, day, hour, monthKey: `${year}-${month}`, dayAttr: `d${day}` };
|
|
1129
|
+
}
|
|
1130
|
+
function buildSitemapHourlyQuery({
|
|
1131
|
+
table,
|
|
1132
|
+
source,
|
|
1133
|
+
hourKey,
|
|
1134
|
+
paths
|
|
1135
|
+
}) {
|
|
1136
|
+
const parts = hourKeyParts2(hourKey);
|
|
1137
|
+
const columns = columnMaps[source];
|
|
1138
|
+
const canonicalUri = canonicalUriExpr(columns.uriStem);
|
|
1139
|
+
const hourPredicate = source === "v2" ? `year = '${parts.year}' AND month = '${parts.month}' AND day = '${parts.day}' AND hour = '${parts.hour}'` : `"date" = DATE '${parts.year}-${parts.month}-${parts.day}' AND substr(${columns.timestamp}, 1, 2) = '${parts.hour}'`;
|
|
1140
|
+
const whereClause = paths.length === 0 ? "1 = 0" : `${hourPredicate} AND ${canonicalUri} IN (${paths.map((path) => `'${escapeSqlString(path)}'`).join(", ")})`;
|
|
1141
|
+
return [
|
|
1142
|
+
"SELECT",
|
|
1143
|
+
` ${canonicalUri} AS path_value,`,
|
|
1144
|
+
" count(*) AS hits",
|
|
1145
|
+
`FROM "${table}"`,
|
|
1146
|
+
`WHERE ${whereClause}`,
|
|
1147
|
+
`GROUP BY ${canonicalUri}`
|
|
1148
|
+
].join(`
|
|
1149
|
+
`);
|
|
1150
|
+
}
|
|
1151
|
+
async function runSitemapHourlyQuery({
|
|
1152
|
+
athena,
|
|
1153
|
+
database,
|
|
1154
|
+
table,
|
|
1155
|
+
workgroup,
|
|
1156
|
+
outputLocation,
|
|
1157
|
+
source,
|
|
1158
|
+
hourKey,
|
|
1159
|
+
paths
|
|
1160
|
+
}) {
|
|
1161
|
+
if (paths.length === 0) {
|
|
1162
|
+
return [];
|
|
1163
|
+
}
|
|
1164
|
+
const queryString = buildSitemapHourlyQuery({ table, source, hourKey, paths });
|
|
1165
|
+
const rawRows = await startAndFetchRows({
|
|
1166
|
+
athena,
|
|
1167
|
+
database,
|
|
1168
|
+
workgroup,
|
|
1169
|
+
outputLocation,
|
|
1170
|
+
queryString
|
|
1171
|
+
});
|
|
1172
|
+
const byPath = {};
|
|
1173
|
+
for (const cells of rawRows) {
|
|
1174
|
+
const path = cells[0];
|
|
1175
|
+
const hits = toCount(cells[1]);
|
|
1176
|
+
if (path == null || hits === 0)
|
|
1177
|
+
continue;
|
|
1178
|
+
byPath[path] = (byPath[path] ?? 0) + hits;
|
|
1179
|
+
}
|
|
1180
|
+
return Object.entries(byPath).map(([path, hits]) => ({ path, hits }));
|
|
1181
|
+
}
|
|
1182
|
+
function buildSitemapMonthlyQuery({
|
|
1183
|
+
table,
|
|
1184
|
+
source,
|
|
1185
|
+
monthKey,
|
|
1186
|
+
paths
|
|
1187
|
+
}) {
|
|
1188
|
+
const match = /^(\d{4})-(\d{2})$/.exec(monthKey);
|
|
1189
|
+
if (match == null) {
|
|
1190
|
+
throw new Error(`Invalid monthKey: ${monthKey}`);
|
|
1191
|
+
}
|
|
1192
|
+
const [, year, month] = match;
|
|
1193
|
+
if (year == null || month == null) {
|
|
1194
|
+
throw new Error(`Invalid monthKey: ${monthKey}`);
|
|
1195
|
+
}
|
|
1196
|
+
const columns = columnMaps[source];
|
|
1197
|
+
const canonicalUri = canonicalUriExpr(columns.uriStem);
|
|
1198
|
+
const dayExpr = source === "v2" ? "day" : `day_of_month(CAST("date" AS date))`;
|
|
1199
|
+
const monthPredicate = source === "v2" ? `year = '${year}' AND month = '${month}'` : `"date" BETWEEN DATE '${year}-${month}-01' AND DATE '${year}-${month}-31'`;
|
|
1200
|
+
const whereClause = paths.length === 0 ? "1 = 0" : `${monthPredicate} AND ${canonicalUri} IN (${paths.map((path) => `'${escapeSqlString(path)}'`).join(", ")})`;
|
|
1201
|
+
return [
|
|
1202
|
+
"SELECT",
|
|
1203
|
+
` ${dayExpr} AS day_value,`,
|
|
1204
|
+
` ${canonicalUri} AS path_value,`,
|
|
1205
|
+
" count(*) AS hits",
|
|
1206
|
+
`FROM "${table}"`,
|
|
1207
|
+
`WHERE ${whereClause}`,
|
|
1208
|
+
`GROUP BY ${dayExpr}, ${canonicalUri}`
|
|
1209
|
+
].join(`
|
|
1210
|
+
`);
|
|
1211
|
+
}
|
|
1212
|
+
async function runSitemapMonthlyQuery({
|
|
1213
|
+
athena,
|
|
1214
|
+
database,
|
|
1215
|
+
table,
|
|
1216
|
+
workgroup,
|
|
1217
|
+
outputLocation,
|
|
1218
|
+
source,
|
|
1219
|
+
monthKey,
|
|
1220
|
+
paths
|
|
1221
|
+
}) {
|
|
1222
|
+
if (paths.length === 0) {
|
|
1223
|
+
return [];
|
|
1224
|
+
}
|
|
1225
|
+
const queryString = buildSitemapMonthlyQuery({ table, source, monthKey, paths });
|
|
1226
|
+
const rawRows = await startAndFetchRows({
|
|
1227
|
+
athena,
|
|
1228
|
+
database,
|
|
1229
|
+
workgroup,
|
|
1230
|
+
outputLocation,
|
|
1231
|
+
queryString
|
|
1232
|
+
});
|
|
1233
|
+
const byPath = {};
|
|
1234
|
+
for (const cells of rawRows) {
|
|
1235
|
+
const dayCell = cells[0];
|
|
1236
|
+
const path = cells[1];
|
|
1237
|
+
const hits = toCount(cells[2]);
|
|
1238
|
+
if (path == null || dayCell == null || hits === 0)
|
|
1239
|
+
continue;
|
|
1240
|
+
const dayKey = String(dayCell).padStart(2, "0");
|
|
1241
|
+
const existing = byPath[path] ?? { days: {}, hits: 0 };
|
|
1242
|
+
existing.days[dayKey] = (existing.days[dayKey] ?? 0) + hits;
|
|
1243
|
+
existing.hits += hits;
|
|
1244
|
+
byPath[path] = existing;
|
|
1245
|
+
}
|
|
1246
|
+
return Object.entries(byPath).map(([path, entry]) => ({
|
|
1247
|
+
path,
|
|
1248
|
+
month: monthKey,
|
|
1249
|
+
days: entry.days,
|
|
1250
|
+
hits: entry.hits
|
|
1251
|
+
}));
|
|
1252
|
+
}
|
|
1253
|
+
async function processSitemapHour({
|
|
1254
|
+
hourKey,
|
|
1255
|
+
source,
|
|
1256
|
+
paths,
|
|
1257
|
+
models,
|
|
1258
|
+
athena,
|
|
1259
|
+
options,
|
|
1260
|
+
dynamo
|
|
1261
|
+
}) {
|
|
1262
|
+
const alreadyApplied = await models.markers.isApplied({ grain: "sitemap", periodKey: hourKey });
|
|
1263
|
+
if (alreadyApplied)
|
|
1264
|
+
return { status: "skipped", urls: 0 };
|
|
1265
|
+
const parts = hourKeyParts2(hourKey);
|
|
1266
|
+
const counts = paths.length === 0 ? [] : await runSitemapHourlyQuery({
|
|
1267
|
+
athena: athena.client,
|
|
1268
|
+
database: athena.database,
|
|
1269
|
+
table: athena.table,
|
|
1270
|
+
workgroup: athena.workgroup,
|
|
1271
|
+
outputLocation: athena.outputLocation,
|
|
1272
|
+
source,
|
|
1273
|
+
hourKey,
|
|
1274
|
+
paths
|
|
1275
|
+
});
|
|
1276
|
+
const ttl = computeMarkerTtl(options?.markerTtlDays);
|
|
1277
|
+
const markerPut = models.markers.buildMarkerPut({
|
|
1278
|
+
grain: "sitemap",
|
|
1279
|
+
periodKey: hourKey,
|
|
1280
|
+
source,
|
|
1281
|
+
ttl
|
|
1282
|
+
});
|
|
1283
|
+
const addUpdates = counts.map((count) => models.sitemapUrlStats.buildAdd({
|
|
1284
|
+
path: count.path,
|
|
1285
|
+
month: parts.monthKey,
|
|
1286
|
+
dayAttr: parts.dayAttr,
|
|
1287
|
+
hits: count.hits
|
|
1288
|
+
}));
|
|
1289
|
+
const result = await sendTransacted({ dynamo, leading: [markerPut], rest: addUpdates });
|
|
1290
|
+
return { status: result.status, urls: result.status === "applied" ? counts.length : 0 };
|
|
1291
|
+
}
|
|
1292
|
+
async function processSitemapMonth({
|
|
1293
|
+
monthKey,
|
|
1294
|
+
source,
|
|
1295
|
+
paths,
|
|
1296
|
+
models,
|
|
1297
|
+
athena
|
|
1298
|
+
}) {
|
|
1299
|
+
if (paths.length === 0) {
|
|
1300
|
+
return { urls: 0 };
|
|
1301
|
+
}
|
|
1302
|
+
const counts = await runSitemapMonthlyQuery({
|
|
1303
|
+
athena: athena.client,
|
|
1304
|
+
database: athena.database,
|
|
1305
|
+
table: athena.table,
|
|
1306
|
+
workgroup: athena.workgroup,
|
|
1307
|
+
outputLocation: athena.outputLocation,
|
|
1308
|
+
source,
|
|
1309
|
+
monthKey,
|
|
1310
|
+
paths
|
|
1311
|
+
});
|
|
1312
|
+
await models.sitemapUrlStats.overwriteMany(counts);
|
|
1313
|
+
return { urls: counts.length };
|
|
1314
|
+
}
|
|
1315
|
+
var sitemapUrlStatSchema = v4.looseObject({
|
|
1316
|
+
pk: v4.string(),
|
|
1317
|
+
sk: v4.string(),
|
|
1318
|
+
path: v4.string(),
|
|
1319
|
+
month: v4.string(),
|
|
1320
|
+
hits: v4.number(),
|
|
1321
|
+
updatedAt: v4.number()
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
class SitemapUrlStats {
|
|
1325
|
+
props;
|
|
1326
|
+
constructor(props) {
|
|
1327
|
+
this.props = props;
|
|
1328
|
+
}
|
|
1329
|
+
buildAdd = ({
|
|
1330
|
+
path,
|
|
1331
|
+
month,
|
|
1332
|
+
dayAttr,
|
|
1333
|
+
hits
|
|
1334
|
+
}) => {
|
|
1335
|
+
const update = {
|
|
1336
|
+
TableName: this.props.tableName,
|
|
1337
|
+
Key: { pk: partitionKey3(path), sk: month },
|
|
1338
|
+
UpdateExpression: "SET #path = :path, #month = :month, #updatedAt = :now ADD #hits :hits, #day :hits",
|
|
1339
|
+
ExpressionAttributeNames: {
|
|
1340
|
+
"#path": "path",
|
|
1341
|
+
"#month": "month",
|
|
1342
|
+
"#updatedAt": "updatedAt",
|
|
1343
|
+
"#hits": "hits",
|
|
1344
|
+
"#day": dayAttr
|
|
1345
|
+
},
|
|
1346
|
+
ExpressionAttributeValues: {
|
|
1347
|
+
":path": path,
|
|
1348
|
+
":month": month,
|
|
1349
|
+
":now": Date.now(),
|
|
1350
|
+
":hits": hits
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
return { Update: update };
|
|
1354
|
+
};
|
|
1355
|
+
overwriteMany = async (stats) => {
|
|
1356
|
+
for (const stat of stats) {
|
|
1357
|
+
await this.props.dynamo.send(new UpdateCommand3(this.buildOverwrite(stat)));
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
buildOverwrite = ({
|
|
1361
|
+
path,
|
|
1362
|
+
month,
|
|
1363
|
+
days,
|
|
1364
|
+
hits
|
|
1365
|
+
}) => {
|
|
1366
|
+
const names = {
|
|
1367
|
+
"#path": "path",
|
|
1368
|
+
"#month": "month",
|
|
1369
|
+
"#hits": "hits",
|
|
1370
|
+
"#updatedAt": "updatedAt"
|
|
1371
|
+
};
|
|
1372
|
+
const valuesMap = {
|
|
1373
|
+
":path": path,
|
|
1374
|
+
":month": month,
|
|
1375
|
+
":hits": hits,
|
|
1376
|
+
":now": Date.now()
|
|
1377
|
+
};
|
|
1378
|
+
const setParts = ["#path = :path", "#month = :month", "#hits = :hits", "#updatedAt = :now"];
|
|
1379
|
+
for (const [dayKey, count] of Object.entries(days)) {
|
|
1380
|
+
const dayAttr = `d${dayKey}`;
|
|
1381
|
+
names[`#${dayAttr}`] = dayAttr;
|
|
1382
|
+
valuesMap[`:${dayAttr}`] = count;
|
|
1383
|
+
setParts.push(`#${dayAttr} = :${dayAttr}`);
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
TableName: this.props.tableName,
|
|
1387
|
+
Key: { pk: partitionKey3(path), sk: month },
|
|
1388
|
+
UpdateExpression: `SET ${setParts.join(", ")}`,
|
|
1389
|
+
ExpressionAttributeNames: names,
|
|
1390
|
+
ExpressionAttributeValues: valuesMap
|
|
1391
|
+
};
|
|
1392
|
+
};
|
|
1393
|
+
getForUrl = async ({
|
|
1394
|
+
path,
|
|
1395
|
+
startMonth,
|
|
1396
|
+
endMonth
|
|
1397
|
+
}) => {
|
|
1398
|
+
const items = await queryAll2({
|
|
1399
|
+
dynamo: this.props.dynamo,
|
|
1400
|
+
input: {
|
|
1401
|
+
TableName: this.props.tableName,
|
|
1402
|
+
KeyConditionExpression: "#pk = :pk AND #sk BETWEEN :start AND :end",
|
|
1403
|
+
ExpressionAttributeNames: { "#pk": "pk", "#sk": "sk" },
|
|
1404
|
+
ExpressionAttributeValues: {
|
|
1405
|
+
":pk": partitionKey3(path),
|
|
1406
|
+
":start": startMonth,
|
|
1407
|
+
":end": endMonth
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
return items.map((item) => this.parseOne(item));
|
|
1412
|
+
};
|
|
1413
|
+
getForMonth = async ({
|
|
1414
|
+
month
|
|
1415
|
+
}) => {
|
|
1416
|
+
if (this.props.reverseIndexName == null) {
|
|
1417
|
+
throw new Error("getForMonth requires a reverseIndexName");
|
|
1418
|
+
}
|
|
1419
|
+
const items = await queryAll2({
|
|
1420
|
+
dynamo: this.props.dynamo,
|
|
1421
|
+
input: {
|
|
1422
|
+
TableName: this.props.tableName,
|
|
1423
|
+
IndexName: this.props.reverseIndexName,
|
|
1424
|
+
KeyConditionExpression: "#sk = :month",
|
|
1425
|
+
ExpressionAttributeNames: { "#sk": "sk" },
|
|
1426
|
+
ExpressionAttributeValues: { ":month": month }
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
return items.map((item) => this.parseOne(item));
|
|
1430
|
+
};
|
|
1431
|
+
parseOne = (item) => {
|
|
1432
|
+
const result = v4.safeParse(sitemapUrlStatSchema, item);
|
|
1433
|
+
if (!result.success) {
|
|
1434
|
+
console.error(v4.flatten(result.issues));
|
|
1435
|
+
throw new Error("Malformed stored sitemap url stat record");
|
|
1436
|
+
}
|
|
1437
|
+
const days = {};
|
|
1438
|
+
for (const [key, value] of Object.entries(result.output)) {
|
|
1439
|
+
const dayMatch = /^d(\d{2})$/.exec(key);
|
|
1440
|
+
if (dayMatch?.[1] != null && typeof value === "number") {
|
|
1441
|
+
days[dayMatch[1]] = value;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return {
|
|
1445
|
+
pk: result.output.pk,
|
|
1446
|
+
sk: result.output.sk,
|
|
1447
|
+
path: result.output.path,
|
|
1448
|
+
month: result.output.month,
|
|
1449
|
+
hits: result.output.hits,
|
|
1450
|
+
updatedAt: result.output.updatedAt,
|
|
1451
|
+
days
|
|
1452
|
+
};
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function partitionKey3(path) {
|
|
1456
|
+
return `sitemapurl#${path}`;
|
|
1457
|
+
}
|
|
1458
|
+
var locPattern = /<loc>([\s\S]*?)<\/loc>/gi;
|
|
1459
|
+
var sitemapIndexPattern = /<sitemapindex/i;
|
|
1460
|
+
var xmlEntities = {
|
|
1461
|
+
"&": "&",
|
|
1462
|
+
"<": "<",
|
|
1463
|
+
">": ">",
|
|
1464
|
+
""": '"',
|
|
1465
|
+
"'": "'"
|
|
1466
|
+
};
|
|
1467
|
+
function unescapeXml(value) {
|
|
1468
|
+
return value.replace(/&|<|>|"|'/g, (entity) => xmlEntities[entity] ?? entity);
|
|
1469
|
+
}
|
|
1470
|
+
function canonicalUriExpr(uriColumn) {
|
|
1471
|
+
return `CASE WHEN length(${uriColumn}) > 1 AND substr(${uriColumn}, -1) = '/' THEN substr(${uriColumn}, 1, length(${uriColumn}) - 1) ELSE ${uriColumn} END`;
|
|
1472
|
+
}
|
|
1473
|
+
function escapeSqlString(value) {
|
|
1474
|
+
return value.replace(/'/g, "''");
|
|
1475
|
+
}
|
|
1476
|
+
function toCount(value) {
|
|
1477
|
+
if (value == null || value === "")
|
|
1478
|
+
return 0;
|
|
1479
|
+
const parsed = Number(value);
|
|
1480
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
1481
|
+
}
|
|
1482
|
+
var defaultMarkerTtlDays = 90;
|
|
1483
|
+
function computeMarkerTtl(markerTtlDays) {
|
|
1484
|
+
const days = markerTtlDays ?? defaultMarkerTtlDays;
|
|
1485
|
+
return Math.floor(Date.now() / 1000) + days * 24 * 60 * 60;
|
|
1486
|
+
}
|
|
1034
1487
|
// packages/analytics-processor/src/time.ts
|
|
1035
1488
|
function pad22(value) {
|
|
1036
1489
|
return value.toString().padStart(2, "0");
|
|
@@ -1116,9 +1569,9 @@ function hoursBetween({ from, to }) {
|
|
|
1116
1569
|
return keys;
|
|
1117
1570
|
}
|
|
1118
1571
|
// packages/analytics-processor/src/totals.ts
|
|
1119
|
-
import { GetCommand as GetCommand3, UpdateCommand as
|
|
1120
|
-
import { queryAll as
|
|
1121
|
-
import * as
|
|
1572
|
+
import { GetCommand as GetCommand3, UpdateCommand as UpdateCommand4 } from "@aws-sdk/lib-dynamodb";
|
|
1573
|
+
import { queryAll as queryAll3 } from "@beesolve/dynamo-helpers";
|
|
1574
|
+
import * as v5 from "valibot";
|
|
1122
1575
|
var additiveFields = [
|
|
1123
1576
|
"hits",
|
|
1124
1577
|
"success",
|
|
@@ -1134,23 +1587,23 @@ var hourlyDetailFields = [
|
|
|
1134
1587
|
"cacheHitRatio",
|
|
1135
1588
|
"uniqueIpCount"
|
|
1136
1589
|
];
|
|
1137
|
-
var totalsSchema =
|
|
1138
|
-
pk:
|
|
1139
|
-
sk:
|
|
1140
|
-
grain:
|
|
1141
|
-
timeKey:
|
|
1142
|
-
hits:
|
|
1143
|
-
success:
|
|
1144
|
-
errors:
|
|
1145
|
-
errors4xx:
|
|
1146
|
-
errors5xx:
|
|
1147
|
-
bytesSent:
|
|
1148
|
-
bytesReceived:
|
|
1149
|
-
p50TimeTaken:
|
|
1150
|
-
p95TimeTaken:
|
|
1151
|
-
cacheHitRatio:
|
|
1152
|
-
uniqueIpCount:
|
|
1153
|
-
ttl:
|
|
1590
|
+
var totalsSchema = v5.object({
|
|
1591
|
+
pk: v5.string(),
|
|
1592
|
+
sk: v5.string(),
|
|
1593
|
+
grain: v5.picklist(grains),
|
|
1594
|
+
timeKey: v5.string(),
|
|
1595
|
+
hits: v5.number(),
|
|
1596
|
+
success: v5.number(),
|
|
1597
|
+
errors: v5.number(),
|
|
1598
|
+
errors4xx: v5.number(),
|
|
1599
|
+
errors5xx: v5.number(),
|
|
1600
|
+
bytesSent: v5.number(),
|
|
1601
|
+
bytesReceived: v5.number(),
|
|
1602
|
+
p50TimeTaken: v5.optional(v5.number()),
|
|
1603
|
+
p95TimeTaken: v5.optional(v5.number()),
|
|
1604
|
+
cacheHitRatio: v5.optional(v5.number()),
|
|
1605
|
+
uniqueIpCount: v5.optional(v5.number()),
|
|
1606
|
+
ttl: v5.optional(v5.number())
|
|
1154
1607
|
});
|
|
1155
1608
|
|
|
1156
1609
|
class Totals {
|
|
@@ -1160,11 +1613,11 @@ class Totals {
|
|
|
1160
1613
|
}
|
|
1161
1614
|
addHourly = async (props) => {
|
|
1162
1615
|
const update = this.buildHourlyUpdate(props);
|
|
1163
|
-
await this.props.dynamo.send(new
|
|
1616
|
+
await this.props.dynamo.send(new UpdateCommand4(update.Update));
|
|
1164
1617
|
};
|
|
1165
1618
|
addAggregate = async (props) => {
|
|
1166
1619
|
const update = this.buildAggregateUpdate(props);
|
|
1167
|
-
await this.props.dynamo.send(new
|
|
1620
|
+
await this.props.dynamo.send(new UpdateCommand4(update.Update));
|
|
1168
1621
|
};
|
|
1169
1622
|
buildHourlyUpdate = (props) => ({
|
|
1170
1623
|
Update: this.buildUpdateMembers({
|
|
@@ -1182,14 +1635,14 @@ class Totals {
|
|
|
1182
1635
|
})
|
|
1183
1636
|
});
|
|
1184
1637
|
queryWindow = async (props) => {
|
|
1185
|
-
const items = await
|
|
1638
|
+
const items = await queryAll3({
|
|
1186
1639
|
dynamo: this.props.dynamo,
|
|
1187
1640
|
input: {
|
|
1188
1641
|
TableName: this.props.tableName,
|
|
1189
1642
|
KeyConditionExpression: "#pk = :pk AND #sk BETWEEN :start AND :end",
|
|
1190
1643
|
ExpressionAttributeNames: { "#pk": "pk", "#sk": "sk" },
|
|
1191
1644
|
ExpressionAttributeValues: {
|
|
1192
|
-
":pk":
|
|
1645
|
+
":pk": partitionKey4(props.grain),
|
|
1193
1646
|
":start": props.startKey,
|
|
1194
1647
|
":end": props.endKey
|
|
1195
1648
|
}
|
|
@@ -1200,7 +1653,7 @@ class Totals {
|
|
|
1200
1653
|
getOne = async (props) => {
|
|
1201
1654
|
const { Item: item } = await this.props.dynamo.send(new GetCommand3({
|
|
1202
1655
|
TableName: this.props.tableName,
|
|
1203
|
-
Key: { pk:
|
|
1656
|
+
Key: { pk: partitionKey4(props.grain), sk: props.timeKey }
|
|
1204
1657
|
}));
|
|
1205
1658
|
if (item == null)
|
|
1206
1659
|
return;
|
|
@@ -1236,28 +1689,29 @@ class Totals {
|
|
|
1236
1689
|
}
|
|
1237
1690
|
return {
|
|
1238
1691
|
TableName: this.props.tableName,
|
|
1239
|
-
Key: { pk:
|
|
1692
|
+
Key: { pk: partitionKey4(props.grain), sk: props.item.timeKey },
|
|
1240
1693
|
UpdateExpression: `SET ${setParts.join(", ")} ADD ${addParts.join(", ")}`,
|
|
1241
1694
|
ExpressionAttributeNames: names,
|
|
1242
1695
|
ExpressionAttributeValues: values
|
|
1243
1696
|
};
|
|
1244
1697
|
};
|
|
1245
1698
|
parseOne = (item) => {
|
|
1246
|
-
const result =
|
|
1699
|
+
const result = v5.safeParse(totalsSchema, item);
|
|
1247
1700
|
if (!result.success) {
|
|
1248
|
-
console.error(
|
|
1701
|
+
console.error(v5.flatten(result.issues));
|
|
1249
1702
|
throw new Error("Malformed stored totals record");
|
|
1250
1703
|
}
|
|
1251
1704
|
return result.output;
|
|
1252
1705
|
};
|
|
1253
1706
|
}
|
|
1254
|
-
function
|
|
1707
|
+
function partitionKey4(grain) {
|
|
1255
1708
|
return `totals#${grain}`;
|
|
1256
1709
|
}
|
|
1257
1710
|
export {
|
|
1258
1711
|
AppliedMarkers,
|
|
1259
1712
|
Breakdowns,
|
|
1260
1713
|
Config,
|
|
1714
|
+
SitemapUrlStats,
|
|
1261
1715
|
Totals,
|
|
1262
1716
|
appliedMarkerSchema,
|
|
1263
1717
|
appliedSources,
|
|
@@ -1266,6 +1720,9 @@ export {
|
|
|
1266
1720
|
breakdownSchema,
|
|
1267
1721
|
bucketStates,
|
|
1268
1722
|
buildHourlyQuery,
|
|
1723
|
+
buildSitemapHourlyQuery,
|
|
1724
|
+
buildSitemapMonthlyQuery,
|
|
1725
|
+
canonicalPath,
|
|
1269
1726
|
chunkTransactItems,
|
|
1270
1727
|
classifyBucketState,
|
|
1271
1728
|
classifyDevice,
|
|
@@ -1273,20 +1730,32 @@ export {
|
|
|
1273
1730
|
configSchema,
|
|
1274
1731
|
dayKey,
|
|
1275
1732
|
dimensions,
|
|
1733
|
+
fetchSitemapPaths,
|
|
1276
1734
|
grainForPeriodKey,
|
|
1277
1735
|
grains,
|
|
1278
1736
|
hourKey,
|
|
1279
1737
|
hourKeyParts,
|
|
1280
1738
|
hoursBetween,
|
|
1739
|
+
isMarkerRaceError,
|
|
1281
1740
|
maxWindowFor,
|
|
1282
1741
|
monthKey,
|
|
1742
|
+
normalizeSitemapLoc,
|
|
1283
1743
|
parsePeriodKey,
|
|
1744
|
+
parseSitemap,
|
|
1284
1745
|
processHour,
|
|
1746
|
+
processSitemapHour,
|
|
1747
|
+
processSitemapMonth,
|
|
1285
1748
|
requiredRecordFields,
|
|
1286
1749
|
resolveBreakdownLive,
|
|
1287
1750
|
resolveTotalsLive,
|
|
1288
1751
|
runHourlyAthenaQuery,
|
|
1752
|
+
runSitemapHourlyQuery,
|
|
1753
|
+
runSitemapMonthlyQuery,
|
|
1754
|
+
sendTransacted,
|
|
1289
1755
|
shiftAnchor,
|
|
1756
|
+
sitemapUrlStatSchema,
|
|
1757
|
+
sitemapUserAgent,
|
|
1758
|
+
startAndFetchRows,
|
|
1290
1759
|
sumBreakdown,
|
|
1291
1760
|
sumTotals,
|
|
1292
1761
|
topNWithOther,
|