@beesolve/analytics-processor 0.3.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 +273 -11
  7. package/dist/index.js +579 -67
  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({
@@ -837,6 +873,7 @@ function toBreakdownRow(item) {
837
873
  return { value: item.value, hits: item.hits, bytes: item.bytes };
838
874
  }
839
875
  // packages/analytics-processor/src/resolveLive.ts
876
+ var bucketStates = ["sealed", "partial", "pending"];
840
877
  var emptyTotals = {
841
878
  hits: 0,
842
879
  success: 0,
@@ -870,21 +907,22 @@ async function resolveTotalsLive({
870
907
  });
871
908
  return sumTotals(hours);
872
909
  }
873
- const dayKeys = dayKeysInProgress({ periodKey, now });
874
- const sealedDays = await models.totals.queryWindow({
910
+ const storedDays = await models.totals.queryWindow({
875
911
  grain: "day",
876
912
  startKey: `${periodKey}-01`,
877
913
  endKey: `${periodKey}-31`
878
914
  });
879
- const sealedByKey = indexByTimeKey(sealedDays);
880
- const resolvedDays = await Promise.all(dayKeys.map(async (dayKey) => {
881
- const stored = sealedByKey[dayKey];
882
- if (stored != null && !isCurrentPeriod({ grain: "day", periodKey: dayKey, now })) {
883
- return pickTotals(stored);
884
- }
885
- return resolveTotalsLive({ models, grain: "day", periodKey: dayKey, now });
886
- }));
887
- return sumTotals(resolvedDays);
915
+ const base = sumTotals(storedDays.filter((item) => !isCurrentPeriod({ grain: "day", periodKey: item.timeKey, now })));
916
+ const currentDayKey = currentChildKey({ parentGrain: "month", periodKey, now });
917
+ if (currentDayKey == null)
918
+ return base;
919
+ const currentDay = await resolveTotalsLive({
920
+ models,
921
+ grain: "day",
922
+ periodKey: currentDayKey,
923
+ now
924
+ });
925
+ return sumTotals([base, currentDay]);
888
926
  }
889
927
  async function resolveBreakdownLive({
890
928
  models,
@@ -901,10 +939,39 @@ async function resolveBreakdownLive({
901
939
  if (stored.length > 0)
902
940
  return toRows(stored);
903
941
  }
904
- const finer = finerGrain(grain);
905
- const childKeys = grain === "day" ? hourKeysInProgress({ periodKey, now }) : dayKeysInProgress({ periodKey, now });
906
- const children = await Promise.all(childKeys.map((childKey) => resolveBreakdownLive({ models, dimension, grain: finer, periodKey: childKey, now })));
907
- return sumBreakdown(children.flat());
942
+ if (grain === "day") {
943
+ const hourKeys = hourKeysInProgress({ periodKey, now });
944
+ const hourRows = await Promise.all(hourKeys.map((hourKey) => models.breakdowns.getForPeriod({ dimension, grain: "hour", periodKey: hourKey })));
945
+ return sumBreakdown(hourRows.flat().map(toRow));
946
+ }
947
+ const storedDayKeys = pastDayKeys({ periodKey, now });
948
+ const storedDayRows = await Promise.all(storedDayKeys.map((dayKey) => models.breakdowns.getForPeriod({ dimension, grain: "day", periodKey: dayKey })));
949
+ const rows = storedDayRows.flat().map(toRow);
950
+ const currentDayKey = currentChildKey({ parentGrain: "month", periodKey, now });
951
+ if (currentDayKey != null) {
952
+ const currentDay = await resolveBreakdownLive({
953
+ models,
954
+ dimension,
955
+ grain: "day",
956
+ periodKey: currentDayKey,
957
+ now
958
+ });
959
+ rows.push(...currentDay);
960
+ }
961
+ return sumBreakdown(rows);
962
+ }
963
+ async function classifyBucketState({
964
+ models,
965
+ grain,
966
+ periodKey,
967
+ now,
968
+ hasData
969
+ }) {
970
+ if (await isSealed({ models, grain, periodKey, now }))
971
+ return "sealed";
972
+ if (hasData)
973
+ return "partial";
974
+ return "pending";
908
975
  }
909
976
  async function isSealed({
910
977
  models,
@@ -912,7 +979,7 @@ async function isSealed({
912
979
  periodKey,
913
980
  now
914
981
  }) {
915
- if (isCurrentPeriod({ grain, periodKey, now }))
982
+ if (!isPastPeriod({ grain, periodKey, now }))
916
983
  return false;
917
984
  return models.markers.isApplied({ grain, periodKey });
918
985
  }
@@ -923,6 +990,13 @@ function isCurrentPeriod({
923
990
  }) {
924
991
  return periodKey === currentKeyFor({ grain, now });
925
992
  }
993
+ function isPastPeriod({
994
+ grain,
995
+ periodKey,
996
+ now
997
+ }) {
998
+ return periodKey < currentKeyFor({ grain, now });
999
+ }
926
1000
  function currentKeyFor({ grain, now }) {
927
1001
  const year = pad4(now.getUTCFullYear());
928
1002
  const month = pad2(now.getUTCMonth() + 1);
@@ -934,8 +1008,14 @@ function currentKeyFor({ grain, now }) {
934
1008
  return `${year}-${month}-${day}`;
935
1009
  return `${year}-${month}-${day}-${hour}`;
936
1010
  }
937
- function finerGrain(grain) {
938
- return grain === "month" ? "day" : "hour";
1011
+ function currentChildKey({
1012
+ parentGrain,
1013
+ periodKey,
1014
+ now
1015
+ }) {
1016
+ if (!isCurrentPeriod({ grain: parentGrain, periodKey, now }))
1017
+ return null;
1018
+ return currentKeyFor({ grain: "day", now });
939
1019
  }
940
1020
  function hourKeysInProgress({
941
1021
  periodKey,
@@ -948,11 +1028,12 @@ function hourKeysInProgress({
948
1028
  }
949
1029
  return keys;
950
1030
  }
951
- function dayKeysInProgress({
1031
+ function pastDayKeys({
952
1032
  periodKey,
953
1033
  now
954
1034
  }) {
955
- const lastDay = isCurrentPeriod({ grain: "month", periodKey, now }) ? now.getUTCDate() : daysInMonth(periodKey);
1035
+ const isCurrentMonth = isCurrentPeriod({ grain: "month", periodKey, now });
1036
+ const lastDay = isCurrentMonth ? now.getUTCDate() - 1 : daysInMonth(periodKey);
956
1037
  const keys = [];
957
1038
  for (let day = 1;day <= lastDay; day += 1) {
958
1039
  keys.push(`${periodKey}-${pad2(day)}`);
@@ -963,15 +1044,11 @@ function daysInMonth(monthKey) {
963
1044
  const [year, month] = monthKey.split("-").map(Number);
964
1045
  return new Date(Date.UTC(year ?? 1970, month ?? 1, 0)).getUTCDate();
965
1046
  }
966
- function indexByTimeKey(items) {
967
- const byKey = {};
968
- for (const item of items) {
969
- byKey[item.timeKey] = item;
970
- }
971
- return byKey;
1047
+ function toRow(item) {
1048
+ return { value: item.value, hits: item.hits, bytes: item.bytes };
972
1049
  }
973
1050
  function toRows(items) {
974
- return items.map((item) => ({ value: item.value, hits: item.hits, bytes: item.bytes }));
1051
+ return items.map(toRow);
975
1052
  }
976
1053
  function pickTotals(item) {
977
1054
  return {
@@ -990,6 +1067,423 @@ function pad2(value) {
990
1067
  function pad4(value) {
991
1068
  return value.toString().padStart(4, "0");
992
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
+ }
993
1487
  // packages/analytics-processor/src/time.ts
994
1488
  function pad22(value) {
995
1489
  return value.toString().padStart(2, "0");
@@ -1075,9 +1569,9 @@ function hoursBetween({ from, to }) {
1075
1569
  return keys;
1076
1570
  }
1077
1571
  // packages/analytics-processor/src/totals.ts
1078
- import { GetCommand as GetCommand3, UpdateCommand as UpdateCommand3 } from "@aws-sdk/lib-dynamodb";
1079
- import { queryAll as queryAll2 } from "@beesolve/dynamo-helpers";
1080
- 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";
1081
1575
  var additiveFields = [
1082
1576
  "hits",
1083
1577
  "success",
@@ -1093,23 +1587,23 @@ var hourlyDetailFields = [
1093
1587
  "cacheHitRatio",
1094
1588
  "uniqueIpCount"
1095
1589
  ];
1096
- var totalsSchema = v4.object({
1097
- pk: v4.string(),
1098
- sk: v4.string(),
1099
- grain: v4.picklist(grains),
1100
- timeKey: v4.string(),
1101
- hits: v4.number(),
1102
- success: v4.number(),
1103
- errors: v4.number(),
1104
- errors4xx: v4.number(),
1105
- errors5xx: v4.number(),
1106
- bytesSent: v4.number(),
1107
- bytesReceived: v4.number(),
1108
- p50TimeTaken: v4.optional(v4.number()),
1109
- p95TimeTaken: v4.optional(v4.number()),
1110
- cacheHitRatio: v4.optional(v4.number()),
1111
- uniqueIpCount: v4.optional(v4.number()),
1112
- 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())
1113
1607
  });
1114
1608
 
1115
1609
  class Totals {
@@ -1119,11 +1613,11 @@ class Totals {
1119
1613
  }
1120
1614
  addHourly = async (props) => {
1121
1615
  const update = this.buildHourlyUpdate(props);
1122
- await this.props.dynamo.send(new UpdateCommand3(update.Update));
1616
+ await this.props.dynamo.send(new UpdateCommand4(update.Update));
1123
1617
  };
1124
1618
  addAggregate = async (props) => {
1125
1619
  const update = this.buildAggregateUpdate(props);
1126
- await this.props.dynamo.send(new UpdateCommand3(update.Update));
1620
+ await this.props.dynamo.send(new UpdateCommand4(update.Update));
1127
1621
  };
1128
1622
  buildHourlyUpdate = (props) => ({
1129
1623
  Update: this.buildUpdateMembers({
@@ -1141,14 +1635,14 @@ class Totals {
1141
1635
  })
1142
1636
  });
1143
1637
  queryWindow = async (props) => {
1144
- const items = await queryAll2({
1638
+ const items = await queryAll3({
1145
1639
  dynamo: this.props.dynamo,
1146
1640
  input: {
1147
1641
  TableName: this.props.tableName,
1148
1642
  KeyConditionExpression: "#pk = :pk AND #sk BETWEEN :start AND :end",
1149
1643
  ExpressionAttributeNames: { "#pk": "pk", "#sk": "sk" },
1150
1644
  ExpressionAttributeValues: {
1151
- ":pk": partitionKey3(props.grain),
1645
+ ":pk": partitionKey4(props.grain),
1152
1646
  ":start": props.startKey,
1153
1647
  ":end": props.endKey
1154
1648
  }
@@ -1159,7 +1653,7 @@ class Totals {
1159
1653
  getOne = async (props) => {
1160
1654
  const { Item: item } = await this.props.dynamo.send(new GetCommand3({
1161
1655
  TableName: this.props.tableName,
1162
- Key: { pk: partitionKey3(props.grain), sk: props.timeKey }
1656
+ Key: { pk: partitionKey4(props.grain), sk: props.timeKey }
1163
1657
  }));
1164
1658
  if (item == null)
1165
1659
  return;
@@ -1195,55 +1689,73 @@ class Totals {
1195
1689
  }
1196
1690
  return {
1197
1691
  TableName: this.props.tableName,
1198
- Key: { pk: partitionKey3(props.grain), sk: props.item.timeKey },
1692
+ Key: { pk: partitionKey4(props.grain), sk: props.item.timeKey },
1199
1693
  UpdateExpression: `SET ${setParts.join(", ")} ADD ${addParts.join(", ")}`,
1200
1694
  ExpressionAttributeNames: names,
1201
1695
  ExpressionAttributeValues: values
1202
1696
  };
1203
1697
  };
1204
1698
  parseOne = (item) => {
1205
- const result = v4.safeParse(totalsSchema, item);
1699
+ const result = v5.safeParse(totalsSchema, item);
1206
1700
  if (!result.success) {
1207
- console.error(v4.flatten(result.issues));
1701
+ console.error(v5.flatten(result.issues));
1208
1702
  throw new Error("Malformed stored totals record");
1209
1703
  }
1210
1704
  return result.output;
1211
1705
  };
1212
1706
  }
1213
- function partitionKey3(grain) {
1707
+ function partitionKey4(grain) {
1214
1708
  return `totals#${grain}`;
1215
1709
  }
1216
1710
  export {
1217
1711
  AppliedMarkers,
1218
1712
  Breakdowns,
1219
1713
  Config,
1714
+ SitemapUrlStats,
1220
1715
  Totals,
1221
1716
  appliedMarkerSchema,
1222
1717
  appliedSources,
1223
1718
  applyDailyRollup,
1224
1719
  applyMonthlyRollup,
1225
1720
  breakdownSchema,
1721
+ bucketStates,
1226
1722
  buildHourlyQuery,
1723
+ buildSitemapHourlyQuery,
1724
+ buildSitemapMonthlyQuery,
1725
+ canonicalPath,
1227
1726
  chunkTransactItems,
1727
+ classifyBucketState,
1228
1728
  classifyDevice,
1229
1729
  columnMaps,
1230
1730
  configSchema,
1231
1731
  dayKey,
1232
1732
  dimensions,
1733
+ fetchSitemapPaths,
1233
1734
  grainForPeriodKey,
1234
1735
  grains,
1235
1736
  hourKey,
1236
1737
  hourKeyParts,
1237
1738
  hoursBetween,
1739
+ isMarkerRaceError,
1238
1740
  maxWindowFor,
1239
1741
  monthKey,
1742
+ normalizeSitemapLoc,
1240
1743
  parsePeriodKey,
1744
+ parseSitemap,
1241
1745
  processHour,
1746
+ processSitemapHour,
1747
+ processSitemapMonth,
1242
1748
  requiredRecordFields,
1243
1749
  resolveBreakdownLive,
1244
1750
  resolveTotalsLive,
1245
1751
  runHourlyAthenaQuery,
1752
+ runSitemapHourlyQuery,
1753
+ runSitemapMonthlyQuery,
1754
+ sendTransacted,
1246
1755
  shiftAnchor,
1756
+ sitemapUrlStatSchema,
1757
+ sitemapUserAgent,
1758
+ startAndFetchRows,
1247
1759
  sumBreakdown,
1248
1760
  sumTotals,
1249
1761
  topNWithOther,