@beesolve/analytics-processor 0.2.6 → 0.3.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/README.md +8 -4
- package/dist/dailyRollup/dailyRollup.mjs +1 -1
- package/dist/dailyRollup/dailyRollup.mjs.map +2 -2
- package/dist/hourly/hourly.mjs +2 -2
- package/dist/hourly/hourly.mjs.map +4 -4
- package/dist/index.d.ts +165 -100
- package/dist/index.js +263 -30
- package/dist/monthlyRollup/monthlyRollup.mjs +1 -1
- package/dist/monthlyRollup/monthlyRollup.mjs.map +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -482,6 +482,57 @@ class Breakdowns {
|
|
|
482
482
|
function partitionKey2(props) {
|
|
483
483
|
return `breakdown#${props.dimension}#${props.grain}#${props.periodKey}`;
|
|
484
484
|
}
|
|
485
|
+
// packages/analytics-processor/src/config.ts
|
|
486
|
+
import { GetCommand as GetCommand2, UpdateCommand as UpdateCommand2 } from "@aws-sdk/lib-dynamodb";
|
|
487
|
+
import * as v3 from "valibot";
|
|
488
|
+
var configSchema = v3.object({
|
|
489
|
+
pk: v3.literal("system#config"),
|
|
490
|
+
sk: v3.literal("analytics"),
|
|
491
|
+
startKey: v3.string(),
|
|
492
|
+
startedAt: v3.string()
|
|
493
|
+
});
|
|
494
|
+
var configKey = { pk: "system#config", sk: "analytics" };
|
|
495
|
+
function isConditionalCheckFailure(error) {
|
|
496
|
+
if (error == null || typeof error !== "object")
|
|
497
|
+
return false;
|
|
498
|
+
const name = "name" in error ? error.name : undefined;
|
|
499
|
+
return name === "ConditionalCheckFailedException";
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
class Config {
|
|
503
|
+
props;
|
|
504
|
+
constructor(props) {
|
|
505
|
+
this.props = props;
|
|
506
|
+
}
|
|
507
|
+
get = async () => {
|
|
508
|
+
const { Item: item } = await this.props.dynamo.send(new GetCommand2({ TableName: this.props.tableName, Key: configKey }));
|
|
509
|
+
if (item == null)
|
|
510
|
+
return null;
|
|
511
|
+
return v3.parse(configSchema, item);
|
|
512
|
+
};
|
|
513
|
+
ensureStart = async (props) => {
|
|
514
|
+
try {
|
|
515
|
+
await this.props.dynamo.send(new UpdateCommand2({
|
|
516
|
+
TableName: this.props.tableName,
|
|
517
|
+
Key: configKey,
|
|
518
|
+
UpdateExpression: "SET #startKey = :hourKey, #startedAt = if_not_exists(#startedAt, :now)",
|
|
519
|
+
ConditionExpression: "attribute_not_exists(#startKey) OR :hourKey < #startKey",
|
|
520
|
+
ExpressionAttributeNames: {
|
|
521
|
+
"#startKey": "startKey",
|
|
522
|
+
"#startedAt": "startedAt"
|
|
523
|
+
},
|
|
524
|
+
ExpressionAttributeValues: {
|
|
525
|
+
":hourKey": props.hourKey,
|
|
526
|
+
":now": new Date().toISOString()
|
|
527
|
+
}
|
|
528
|
+
}));
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (isConditionalCheckFailure(error))
|
|
531
|
+
return;
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
485
536
|
// packages/analytics-processor/src/device.ts
|
|
486
537
|
var botPattern = /bot|crawler|spider|crawl|slurp|mediapartners|facebookexternalhit|bingpreview|duckduckbot|baiduspider|yandex|semrush|ahrefs|headlesschrome|python-requests|curl|wget|monitor|pingdom|uptime/i;
|
|
487
538
|
var mobilePattern = /mobi|android|iphone|ipad|ipod|iemobile|blackberry|opera mini|windows phone|webos/i;
|
|
@@ -562,11 +613,15 @@ async function processHour({
|
|
|
562
613
|
byPath: query.byPath,
|
|
563
614
|
byCountry
|
|
564
615
|
});
|
|
565
|
-
|
|
616
|
+
const result = await sendTransacted({
|
|
566
617
|
dynamo,
|
|
567
618
|
leading: [markerPut, totalsUpdate],
|
|
568
619
|
rest: breakdownUpdates
|
|
569
620
|
});
|
|
621
|
+
if (result.status === "applied" && query.totals.hits > 0 && models.config != null) {
|
|
622
|
+
await models.config.ensureStart({ hourKey });
|
|
623
|
+
}
|
|
624
|
+
return result;
|
|
570
625
|
}
|
|
571
626
|
async function applyDailyRollup({
|
|
572
627
|
dayKey,
|
|
@@ -781,24 +836,196 @@ function computeHourlyTtl(hourlyTtlDays) {
|
|
|
781
836
|
function toBreakdownRow(item) {
|
|
782
837
|
return { value: item.value, hits: item.hits, bytes: item.bytes };
|
|
783
838
|
}
|
|
784
|
-
// packages/analytics-processor/src/
|
|
839
|
+
// packages/analytics-processor/src/resolveLive.ts
|
|
840
|
+
var emptyTotals = {
|
|
841
|
+
hits: 0,
|
|
842
|
+
success: 0,
|
|
843
|
+
errors: 0,
|
|
844
|
+
errors4xx: 0,
|
|
845
|
+
errors5xx: 0,
|
|
846
|
+
bytesSent: 0,
|
|
847
|
+
bytesReceived: 0
|
|
848
|
+
};
|
|
849
|
+
async function resolveTotalsLive({
|
|
850
|
+
models,
|
|
851
|
+
grain,
|
|
852
|
+
periodKey,
|
|
853
|
+
now
|
|
854
|
+
}) {
|
|
855
|
+
if (grain === "hour") {
|
|
856
|
+
const stored = await models.totals.getOne({ grain: "hour", timeKey: periodKey });
|
|
857
|
+
return stored == null ? emptyTotals : pickTotals(stored);
|
|
858
|
+
}
|
|
859
|
+
if (await isSealed({ models, grain, periodKey, now })) {
|
|
860
|
+
const stored = await models.totals.getOne({ grain, timeKey: periodKey });
|
|
861
|
+
if (stored != null)
|
|
862
|
+
return pickTotals(stored);
|
|
863
|
+
}
|
|
864
|
+
if (grain === "day") {
|
|
865
|
+
const lastHour = isCurrentPeriod({ grain: "day", periodKey, now }) ? now.getUTCHours() : 23;
|
|
866
|
+
const hours = await models.totals.queryWindow({
|
|
867
|
+
grain: "hour",
|
|
868
|
+
startKey: `${periodKey}-00`,
|
|
869
|
+
endKey: `${periodKey}-${pad2(lastHour)}`
|
|
870
|
+
});
|
|
871
|
+
return sumTotals(hours);
|
|
872
|
+
}
|
|
873
|
+
const dayKeys = dayKeysInProgress({ periodKey, now });
|
|
874
|
+
const sealedDays = await models.totals.queryWindow({
|
|
875
|
+
grain: "day",
|
|
876
|
+
startKey: `${periodKey}-01`,
|
|
877
|
+
endKey: `${periodKey}-31`
|
|
878
|
+
});
|
|
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);
|
|
888
|
+
}
|
|
889
|
+
async function resolveBreakdownLive({
|
|
890
|
+
models,
|
|
891
|
+
dimension,
|
|
892
|
+
grain,
|
|
893
|
+
periodKey,
|
|
894
|
+
now
|
|
895
|
+
}) {
|
|
896
|
+
if (grain === "hour") {
|
|
897
|
+
return toRows(await models.breakdowns.getForPeriod({ dimension, grain: "hour", periodKey }));
|
|
898
|
+
}
|
|
899
|
+
if (await isSealed({ models, grain, periodKey, now })) {
|
|
900
|
+
const stored = await models.breakdowns.getForPeriod({ dimension, grain, periodKey });
|
|
901
|
+
if (stored.length > 0)
|
|
902
|
+
return toRows(stored);
|
|
903
|
+
}
|
|
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());
|
|
908
|
+
}
|
|
909
|
+
async function isSealed({
|
|
910
|
+
models,
|
|
911
|
+
grain,
|
|
912
|
+
periodKey,
|
|
913
|
+
now
|
|
914
|
+
}) {
|
|
915
|
+
if (isCurrentPeriod({ grain, periodKey, now }))
|
|
916
|
+
return false;
|
|
917
|
+
return models.markers.isApplied({ grain, periodKey });
|
|
918
|
+
}
|
|
919
|
+
function isCurrentPeriod({
|
|
920
|
+
grain,
|
|
921
|
+
periodKey,
|
|
922
|
+
now
|
|
923
|
+
}) {
|
|
924
|
+
return periodKey === currentKeyFor({ grain, now });
|
|
925
|
+
}
|
|
926
|
+
function currentKeyFor({ grain, now }) {
|
|
927
|
+
const year = pad4(now.getUTCFullYear());
|
|
928
|
+
const month = pad2(now.getUTCMonth() + 1);
|
|
929
|
+
const day = pad2(now.getUTCDate());
|
|
930
|
+
const hour = pad2(now.getUTCHours());
|
|
931
|
+
if (grain === "month")
|
|
932
|
+
return `${year}-${month}`;
|
|
933
|
+
if (grain === "day")
|
|
934
|
+
return `${year}-${month}-${day}`;
|
|
935
|
+
return `${year}-${month}-${day}-${hour}`;
|
|
936
|
+
}
|
|
937
|
+
function finerGrain(grain) {
|
|
938
|
+
return grain === "month" ? "day" : "hour";
|
|
939
|
+
}
|
|
940
|
+
function hourKeysInProgress({
|
|
941
|
+
periodKey,
|
|
942
|
+
now
|
|
943
|
+
}) {
|
|
944
|
+
const lastHour = isCurrentPeriod({ grain: "day", periodKey, now }) ? now.getUTCHours() : 23;
|
|
945
|
+
const keys = [];
|
|
946
|
+
for (let hour = 0;hour <= lastHour; hour += 1) {
|
|
947
|
+
keys.push(`${periodKey}-${pad2(hour)}`);
|
|
948
|
+
}
|
|
949
|
+
return keys;
|
|
950
|
+
}
|
|
951
|
+
function dayKeysInProgress({
|
|
952
|
+
periodKey,
|
|
953
|
+
now
|
|
954
|
+
}) {
|
|
955
|
+
const lastDay = isCurrentPeriod({ grain: "month", periodKey, now }) ? now.getUTCDate() : daysInMonth(periodKey);
|
|
956
|
+
const keys = [];
|
|
957
|
+
for (let day = 1;day <= lastDay; day += 1) {
|
|
958
|
+
keys.push(`${periodKey}-${pad2(day)}`);
|
|
959
|
+
}
|
|
960
|
+
return keys;
|
|
961
|
+
}
|
|
962
|
+
function daysInMonth(monthKey) {
|
|
963
|
+
const [year, month] = monthKey.split("-").map(Number);
|
|
964
|
+
return new Date(Date.UTC(year ?? 1970, month ?? 1, 0)).getUTCDate();
|
|
965
|
+
}
|
|
966
|
+
function indexByTimeKey(items) {
|
|
967
|
+
const byKey = {};
|
|
968
|
+
for (const item of items) {
|
|
969
|
+
byKey[item.timeKey] = item;
|
|
970
|
+
}
|
|
971
|
+
return byKey;
|
|
972
|
+
}
|
|
973
|
+
function toRows(items) {
|
|
974
|
+
return items.map((item) => ({ value: item.value, hits: item.hits, bytes: item.bytes }));
|
|
975
|
+
}
|
|
976
|
+
function pickTotals(item) {
|
|
977
|
+
return {
|
|
978
|
+
hits: item.hits,
|
|
979
|
+
success: item.success,
|
|
980
|
+
errors: item.errors,
|
|
981
|
+
errors4xx: item.errors4xx,
|
|
982
|
+
errors5xx: item.errors5xx,
|
|
983
|
+
bytesSent: item.bytesSent,
|
|
984
|
+
bytesReceived: item.bytesReceived
|
|
985
|
+
};
|
|
986
|
+
}
|
|
785
987
|
function pad2(value) {
|
|
786
988
|
return value.toString().padStart(2, "0");
|
|
787
989
|
}
|
|
788
990
|
function pad4(value) {
|
|
789
991
|
return value.toString().padStart(4, "0");
|
|
790
992
|
}
|
|
993
|
+
// packages/analytics-processor/src/time.ts
|
|
994
|
+
function pad22(value) {
|
|
995
|
+
return value.toString().padStart(2, "0");
|
|
996
|
+
}
|
|
997
|
+
function pad42(value) {
|
|
998
|
+
return value.toString().padStart(4, "0");
|
|
999
|
+
}
|
|
791
1000
|
function yearKey(date) {
|
|
792
|
-
return
|
|
1001
|
+
return pad42(date.getUTCFullYear());
|
|
793
1002
|
}
|
|
794
1003
|
function monthKey(date) {
|
|
795
|
-
return `${
|
|
1004
|
+
return `${pad42(date.getUTCFullYear())}-${pad22(date.getUTCMonth() + 1)}`;
|
|
796
1005
|
}
|
|
797
1006
|
function dayKey(date) {
|
|
798
|
-
return `${monthKey(date)}-${
|
|
1007
|
+
return `${monthKey(date)}-${pad22(date.getUTCDate())}`;
|
|
799
1008
|
}
|
|
800
1009
|
function hourKey(date) {
|
|
801
|
-
return `${dayKey(date)}-${
|
|
1010
|
+
return `${dayKey(date)}-${pad22(date.getUTCHours())}`;
|
|
1011
|
+
}
|
|
1012
|
+
function parsePeriodKey(periodKey) {
|
|
1013
|
+
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(periodKey);
|
|
1014
|
+
if (match == null) {
|
|
1015
|
+
throw new Error(`Invalid period key: ${periodKey}`);
|
|
1016
|
+
}
|
|
1017
|
+
const [, year, month, day, hour] = match;
|
|
1018
|
+
return new Date(Date.UTC(Number(year), month == null ? 0 : Number(month) - 1, day == null ? 1 : Number(day), hour == null ? 0 : Number(hour)));
|
|
1019
|
+
}
|
|
1020
|
+
function grainForPeriodKey(periodKey) {
|
|
1021
|
+
const segments = periodKey.split("-").length;
|
|
1022
|
+
if (segments === 1)
|
|
1023
|
+
return "year";
|
|
1024
|
+
if (segments === 2)
|
|
1025
|
+
return "month";
|
|
1026
|
+
if (segments === 3)
|
|
1027
|
+
return "day";
|
|
1028
|
+
return "hour";
|
|
802
1029
|
}
|
|
803
1030
|
function maxWindowFor(grain) {
|
|
804
1031
|
if (grain === "hour")
|
|
@@ -848,9 +1075,9 @@ function hoursBetween({ from, to }) {
|
|
|
848
1075
|
return keys;
|
|
849
1076
|
}
|
|
850
1077
|
// packages/analytics-processor/src/totals.ts
|
|
851
|
-
import { GetCommand as
|
|
1078
|
+
import { GetCommand as GetCommand3, UpdateCommand as UpdateCommand3 } from "@aws-sdk/lib-dynamodb";
|
|
852
1079
|
import { queryAll as queryAll2 } from "@beesolve/dynamo-helpers";
|
|
853
|
-
import * as
|
|
1080
|
+
import * as v4 from "valibot";
|
|
854
1081
|
var additiveFields = [
|
|
855
1082
|
"hits",
|
|
856
1083
|
"success",
|
|
@@ -866,23 +1093,23 @@ var hourlyDetailFields = [
|
|
|
866
1093
|
"cacheHitRatio",
|
|
867
1094
|
"uniqueIpCount"
|
|
868
1095
|
];
|
|
869
|
-
var totalsSchema =
|
|
870
|
-
pk:
|
|
871
|
-
sk:
|
|
872
|
-
grain:
|
|
873
|
-
timeKey:
|
|
874
|
-
hits:
|
|
875
|
-
success:
|
|
876
|
-
errors:
|
|
877
|
-
errors4xx:
|
|
878
|
-
errors5xx:
|
|
879
|
-
bytesSent:
|
|
880
|
-
bytesReceived:
|
|
881
|
-
p50TimeTaken:
|
|
882
|
-
p95TimeTaken:
|
|
883
|
-
cacheHitRatio:
|
|
884
|
-
uniqueIpCount:
|
|
885
|
-
ttl:
|
|
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())
|
|
886
1113
|
});
|
|
887
1114
|
|
|
888
1115
|
class Totals {
|
|
@@ -892,11 +1119,11 @@ class Totals {
|
|
|
892
1119
|
}
|
|
893
1120
|
addHourly = async (props) => {
|
|
894
1121
|
const update = this.buildHourlyUpdate(props);
|
|
895
|
-
await this.props.dynamo.send(new
|
|
1122
|
+
await this.props.dynamo.send(new UpdateCommand3(update.Update));
|
|
896
1123
|
};
|
|
897
1124
|
addAggregate = async (props) => {
|
|
898
1125
|
const update = this.buildAggregateUpdate(props);
|
|
899
|
-
await this.props.dynamo.send(new
|
|
1126
|
+
await this.props.dynamo.send(new UpdateCommand3(update.Update));
|
|
900
1127
|
};
|
|
901
1128
|
buildHourlyUpdate = (props) => ({
|
|
902
1129
|
Update: this.buildUpdateMembers({
|
|
@@ -930,7 +1157,7 @@ class Totals {
|
|
|
930
1157
|
return items.map((item) => this.parseOne(item));
|
|
931
1158
|
};
|
|
932
1159
|
getOne = async (props) => {
|
|
933
|
-
const { Item: item } = await this.props.dynamo.send(new
|
|
1160
|
+
const { Item: item } = await this.props.dynamo.send(new GetCommand3({
|
|
934
1161
|
TableName: this.props.tableName,
|
|
935
1162
|
Key: { pk: partitionKey3(props.grain), sk: props.timeKey }
|
|
936
1163
|
}));
|
|
@@ -975,9 +1202,9 @@ class Totals {
|
|
|
975
1202
|
};
|
|
976
1203
|
};
|
|
977
1204
|
parseOne = (item) => {
|
|
978
|
-
const result =
|
|
1205
|
+
const result = v4.safeParse(totalsSchema, item);
|
|
979
1206
|
if (!result.success) {
|
|
980
|
-
console.error(
|
|
1207
|
+
console.error(v4.flatten(result.issues));
|
|
981
1208
|
throw new Error("Malformed stored totals record");
|
|
982
1209
|
}
|
|
983
1210
|
return result.output;
|
|
@@ -989,6 +1216,7 @@ function partitionKey3(grain) {
|
|
|
989
1216
|
export {
|
|
990
1217
|
AppliedMarkers,
|
|
991
1218
|
Breakdowns,
|
|
1219
|
+
Config,
|
|
992
1220
|
Totals,
|
|
993
1221
|
appliedMarkerSchema,
|
|
994
1222
|
appliedSources,
|
|
@@ -999,16 +1227,21 @@ export {
|
|
|
999
1227
|
chunkTransactItems,
|
|
1000
1228
|
classifyDevice,
|
|
1001
1229
|
columnMaps,
|
|
1230
|
+
configSchema,
|
|
1002
1231
|
dayKey,
|
|
1003
1232
|
dimensions,
|
|
1233
|
+
grainForPeriodKey,
|
|
1004
1234
|
grains,
|
|
1005
1235
|
hourKey,
|
|
1006
1236
|
hourKeyParts,
|
|
1007
1237
|
hoursBetween,
|
|
1008
1238
|
maxWindowFor,
|
|
1009
1239
|
monthKey,
|
|
1240
|
+
parsePeriodKey,
|
|
1010
1241
|
processHour,
|
|
1011
1242
|
requiredRecordFields,
|
|
1243
|
+
resolveBreakdownLive,
|
|
1244
|
+
resolveTotalsLive,
|
|
1012
1245
|
runHourlyAthenaQuery,
|
|
1013
1246
|
shiftAnchor,
|
|
1014
1247
|
sumBreakdown,
|