@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.
Files changed (75) hide show
  1. package/DOCS.md +23 -3
  2. package/dist/cdk.js +46 -0
  3. package/dist/dailyRollup/dailyRollup.mjs.map +2 -2
  4. package/dist/hourly/hourly.mjs +3 -3
  5. package/dist/hourly/hourly.mjs.map +3 -3
  6. package/dist/index.d.ts +233 -3
  7. package/dist/index.js +509 -40
  8. package/dist/monthlyRollup/monthlyRollup.mjs.map +2 -2
  9. package/dist/sitemap/bowser-UJEXXM6S.mjs +2 -0
  10. package/dist/sitemap/bowser-UJEXXM6S.mjs.map +7 -0
  11. package/dist/sitemap/chunk-3UGRDNDZ.mjs +6 -0
  12. package/dist/sitemap/chunk-3UGRDNDZ.mjs.map +7 -0
  13. package/dist/sitemap/chunk-BH7ZEBAB.mjs +2 -0
  14. package/dist/sitemap/chunk-BH7ZEBAB.mjs.map +7 -0
  15. package/dist/sitemap/chunk-CFZ5JWD3.mjs +2 -0
  16. package/dist/sitemap/chunk-CFZ5JWD3.mjs.map +7 -0
  17. package/dist/sitemap/chunk-KRS5YV2Z.mjs +2 -0
  18. package/dist/sitemap/chunk-KRS5YV2Z.mjs.map +7 -0
  19. package/dist/sitemap/chunk-LB33VBGK.mjs +2 -0
  20. package/dist/sitemap/chunk-LB33VBGK.mjs.map +7 -0
  21. package/dist/sitemap/chunk-OC3UN3OS.mjs +2 -0
  22. package/dist/sitemap/chunk-OC3UN3OS.mjs.map +7 -0
  23. package/dist/sitemap/chunk-PD2XGJQD.mjs +10 -0
  24. package/dist/sitemap/chunk-PD2XGJQD.mjs.map +7 -0
  25. package/dist/sitemap/chunk-QPD5NJHJ.mjs +2 -0
  26. package/dist/sitemap/chunk-QPD5NJHJ.mjs.map +7 -0
  27. package/dist/sitemap/chunk-REMPUQ6B.mjs +19 -0
  28. package/dist/sitemap/chunk-REMPUQ6B.mjs.map +7 -0
  29. package/dist/sitemap/chunk-SNCZ4GCT.mjs +2 -0
  30. package/dist/sitemap/chunk-SNCZ4GCT.mjs.map +7 -0
  31. package/dist/sitemap/chunk-TZ6TJSKE.mjs +10 -0
  32. package/dist/sitemap/chunk-TZ6TJSKE.mjs.map +7 -0
  33. package/dist/sitemap/chunk-YMGL4RSC.mjs +2 -0
  34. package/dist/sitemap/chunk-YMGL4RSC.mjs.map +7 -0
  35. package/dist/sitemap/chunk-YY2HYMK3.mjs +4 -0
  36. package/dist/sitemap/chunk-YY2HYMK3.mjs.map +7 -0
  37. package/dist/sitemap/client-WGCJRHWA.mjs +2 -0
  38. package/dist/sitemap/client-WGCJRHWA.mjs.map +7 -0
  39. package/dist/sitemap/dist-es-3FAKVHIH.mjs +2 -0
  40. package/dist/sitemap/dist-es-3FAKVHIH.mjs.map +7 -0
  41. package/dist/sitemap/dist-es-A36NQ5CO.mjs +2 -0
  42. package/dist/sitemap/dist-es-A36NQ5CO.mjs.map +7 -0
  43. package/dist/sitemap/dist-es-F7GXZJIC.mjs +3 -0
  44. package/dist/sitemap/dist-es-F7GXZJIC.mjs.map +7 -0
  45. package/dist/sitemap/dist-es-GRXF7RTK.mjs +2 -0
  46. package/dist/sitemap/dist-es-GRXF7RTK.mjs.map +7 -0
  47. package/dist/sitemap/dist-es-OKUHAI6O.mjs +2 -0
  48. package/dist/sitemap/dist-es-OKUHAI6O.mjs.map +7 -0
  49. package/dist/sitemap/dist-es-P7BXOYHO.mjs +2 -0
  50. package/dist/sitemap/dist-es-P7BXOYHO.mjs.map +7 -0
  51. package/dist/sitemap/dist-es-SQ53M5JX.mjs +7 -0
  52. package/dist/sitemap/dist-es-SQ53M5JX.mjs.map +7 -0
  53. package/dist/sitemap/dist-es-TCGA3UGX.mjs +2 -0
  54. package/dist/sitemap/dist-es-TCGA3UGX.mjs.map +7 -0
  55. package/dist/sitemap/dist-es-UXKF5GQY.mjs +3 -0
  56. package/dist/sitemap/dist-es-UXKF5GQY.mjs.map +7 -0
  57. package/dist/sitemap/dist-es-YJ4PQZC7.mjs +2 -0
  58. package/dist/sitemap/dist-es-YJ4PQZC7.mjs.map +7 -0
  59. package/dist/sitemap/event-streams-6V3Q6GHC.mjs +2 -0
  60. package/dist/sitemap/event-streams-6V3Q6GHC.mjs.map +7 -0
  61. package/dist/sitemap/loadCognitoIdentity-RIENBHOX.mjs +2 -0
  62. package/dist/sitemap/loadCognitoIdentity-RIENBHOX.mjs.map +7 -0
  63. package/dist/sitemap/loadSso-DTQ4R4CV.mjs +2 -0
  64. package/dist/sitemap/loadSso-DTQ4R4CV.mjs.map +7 -0
  65. package/dist/sitemap/loadSts-GZDTRQPT.mjs +2 -0
  66. package/dist/sitemap/loadSts-GZDTRQPT.mjs.map +7 -0
  67. package/dist/sitemap/signin-6D536OAO.mjs +2 -0
  68. package/dist/sitemap/signin-6D536OAO.mjs.map +7 -0
  69. package/dist/sitemap/sitemap.mjs +3 -0
  70. package/dist/sitemap/sitemap.mjs.map +7 -0
  71. package/dist/sitemap/sso-oidc-T5CK3NX3.mjs +2 -0
  72. package/dist/sitemap/sso-oidc-T5CK3NX3.mjs.map +7 -0
  73. package/dist/sitemap/sts-62ECLMPL.mjs +2 -0
  74. package/dist/sitemap/sts-62ECLMPL.mjs.map +7 -0
  75. 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 fetchRows({
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
- const cells = (dataRow.Data ?? []).map((datum) => datum.VarCharValue);
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 runHourlyAthenaQuery({
350
+ async function startAndFetchRows({
352
351
  athena,
353
352
  database,
354
- table,
355
353
  workgroup,
356
354
  outputLocation,
357
- source,
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
- const rows = await fetchRows({ athena, queryExecutionId });
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
+ "&amp;": "&",
1462
+ "&lt;": "<",
1463
+ "&gt;": ">",
1464
+ "&quot;": '"',
1465
+ "&#39;": "'"
1466
+ };
1467
+ function unescapeXml(value) {
1468
+ return value.replace(/&amp;|&lt;|&gt;|&quot;|&#39;/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 UpdateCommand3 } from "@aws-sdk/lib-dynamodb";
1120
- import { queryAll as queryAll2 } from "@beesolve/dynamo-helpers";
1121
- import * as v4 from "valibot";
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 = v4.object({
1138
- pk: v4.string(),
1139
- sk: v4.string(),
1140
- grain: v4.picklist(grains),
1141
- timeKey: v4.string(),
1142
- hits: v4.number(),
1143
- success: v4.number(),
1144
- errors: v4.number(),
1145
- errors4xx: v4.number(),
1146
- errors5xx: v4.number(),
1147
- bytesSent: v4.number(),
1148
- bytesReceived: v4.number(),
1149
- p50TimeTaken: v4.optional(v4.number()),
1150
- p95TimeTaken: v4.optional(v4.number()),
1151
- cacheHitRatio: v4.optional(v4.number()),
1152
- uniqueIpCount: v4.optional(v4.number()),
1153
- ttl: v4.optional(v4.number())
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 UpdateCommand3(update.Update));
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 UpdateCommand3(update.Update));
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 queryAll2({
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": partitionKey3(props.grain),
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: partitionKey3(props.grain), sk: props.timeKey }
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: partitionKey3(props.grain), sk: props.item.timeKey },
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 = v4.safeParse(totalsSchema, item);
1699
+ const result = v5.safeParse(totalsSchema, item);
1247
1700
  if (!result.success) {
1248
- console.error(v4.flatten(result.issues));
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 partitionKey3(grain) {
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,