@beesolve/analytics-processor 0.3.0 → 0.4.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/dist/index.d.ts +41 -9
- package/dist/index.js +70 -27
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -363,6 +363,18 @@ interface RollupModels {
|
|
|
363
363
|
readonly markers: Pick<AppliedMarkers, "isApplied" | "buildMarkerPut">;
|
|
364
364
|
}
|
|
365
365
|
type View = Grain | "year";
|
|
366
|
+
/**
|
|
367
|
+
* How a bucket's value was obtained, so the UI can tell "no traffic" apart from
|
|
368
|
+
* "not calculated yet":
|
|
369
|
+
* - `sealed`: a finalized, rolled-up period read from its stored aggregate. A
|
|
370
|
+
* zero-hit sealed bucket is genuine "no traffic".
|
|
371
|
+
* - `partial`: an in-progress period computed live from the finer grain; the
|
|
372
|
+
* value is real but may still grow.
|
|
373
|
+
* - `pending`: a period past its expected processing time with no stored data
|
|
374
|
+
* and nothing computable yet — awaiting processing.
|
|
375
|
+
*/
|
|
376
|
+
declare const bucketStates: readonly ["sealed", "partial", "pending"];
|
|
377
|
+
type BucketState = (typeof bucketStates)[number];
|
|
366
378
|
interface ResolveModels {
|
|
367
379
|
readonly totals: Pick<Totals, "queryWindow" | "getOne">;
|
|
368
380
|
readonly breakdowns: Pick<Breakdowns, "getForPeriod">;
|
|
@@ -370,15 +382,21 @@ interface ResolveModels {
|
|
|
370
382
|
}
|
|
371
383
|
/**
|
|
372
384
|
* Resolves one bucket's totals, computing an in-progress period on the fly from
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
385
|
+
* exactly one grain finer, following only the current edge deeper.
|
|
386
|
+
*
|
|
387
|
+
* A sealed period (strictly past, rollup marker applied) is read from its stored
|
|
388
|
+
* aggregate. Otherwise the period is the sum of its children at the next finer
|
|
389
|
+
* grain, where every already-complete child is taken from its stored aggregate
|
|
390
|
+
* and only the single in-progress child is resolved recursively:
|
|
377
391
|
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
392
|
+
* - partial day = stored/queried hours `00..currentHour`
|
|
393
|
+
* - partial month = stored days `01..currentDay-1` + the partial current day
|
|
394
|
+
* - partial year = stored months `01..currentMonth-1` + the partial current month
|
|
395
|
+
*
|
|
396
|
+
* So a partial month never fans out to hours for its finished days (it trusts
|
|
397
|
+
* their day aggregate) — only the one current day drills into hours. Each level
|
|
398
|
+
* is one ranged query, keeping the read count bounded (a year touches ~12
|
|
399
|
+
* months + ~31 days + ~24 hours) and the request sub-second.
|
|
382
400
|
*/
|
|
383
401
|
declare function resolveTotalsLive({ models, grain, periodKey, now }: {
|
|
384
402
|
readonly models: ResolveModels;
|
|
@@ -393,6 +411,20 @@ declare function resolveBreakdownLive({ models, dimension, grain, periodKey, now
|
|
|
393
411
|
readonly periodKey: string;
|
|
394
412
|
readonly now: Date;
|
|
395
413
|
}): Promise<Array<BreakdownRow>>;
|
|
414
|
+
/**
|
|
415
|
+
* Classifies a bucket as sealed / partial / pending without recomputing its
|
|
416
|
+
* value. A sealed bucket is a finalized rollup (its marker is applied), so a
|
|
417
|
+
* zero-hit sealed bucket is genuine "no traffic". A non-sealed bucket that
|
|
418
|
+
* yielded data is in-progress (`partial`); one with nothing to show is awaiting
|
|
419
|
+
* processing (`pending`).
|
|
420
|
+
*/
|
|
421
|
+
declare function classifyBucketState({ models, grain, periodKey, now, hasData }: {
|
|
422
|
+
readonly models: ResolveModels;
|
|
423
|
+
readonly grain: Grain;
|
|
424
|
+
readonly periodKey: string;
|
|
425
|
+
readonly now: Date;
|
|
426
|
+
readonly hasData: boolean;
|
|
427
|
+
}): Promise<BucketState>;
|
|
396
428
|
declare function yearKey(date: Date): string;
|
|
397
429
|
declare function monthKey2(date: Date): string;
|
|
398
430
|
declare function dayKey2(date: Date): string;
|
|
@@ -423,4 +455,4 @@ declare function hoursBetween({ from, to }: {
|
|
|
423
455
|
from: Date;
|
|
424
456
|
to: Date;
|
|
425
457
|
}): Array<string>;
|
|
426
|
-
export { AnalyticsConfig, AppliedMarker, AppliedMarkers, AppliedSource, AthenaColumnMap, BreakdownInput, BreakdownItem, BreakdownRow, Breakdowns, Config, DeviceBucket, Dimension, Grain, HourlyAthenaResult, HourlyAthenaTotals, ProcessHourAthena, ProcessHourModels, ProcessHourOptions, ProcessHourResult, ResolveModels, SummedTotals, Totals, TotalsInput, TotalsItem, TransactItem, View, appliedMarkerSchema, appliedSources, applyDailyRollup, applyMonthlyRollup, breakdownSchema, buildHourlyQuery, chunkTransactItems, classifyDevice, columnMaps, configSchema, dayKey2 as dayKey, dimensions, grainForPeriodKey, grains, hourKey2 as hourKey, hourKeyParts, hoursBetween, maxWindowFor, monthKey2 as monthKey, parsePeriodKey, processHour, requiredRecordFields, resolveBreakdownLive, resolveTotalsLive, runHourlyAthenaQuery, shiftAnchor, sumBreakdown, sumTotals, topNWithOther, totalsSchema, windowKeys, yearKey };
|
|
458
|
+
export { AnalyticsConfig, AppliedMarker, AppliedMarkers, AppliedSource, AthenaColumnMap, BreakdownInput, BreakdownItem, BreakdownRow, Breakdowns, BucketState, Config, DeviceBucket, Dimension, Grain, HourlyAthenaResult, HourlyAthenaTotals, ProcessHourAthena, ProcessHourModels, ProcessHourOptions, ProcessHourResult, ResolveModels, SummedTotals, Totals, TotalsInput, TotalsItem, TransactItem, View, appliedMarkerSchema, appliedSources, applyDailyRollup, applyMonthlyRollup, breakdownSchema, bucketStates, buildHourlyQuery, chunkTransactItems, classifyBucketState, classifyDevice, columnMaps, configSchema, dayKey2 as dayKey, dimensions, grainForPeriodKey, grains, hourKey2 as hourKey, hourKeyParts, hoursBetween, maxWindowFor, monthKey2 as monthKey, parsePeriodKey, processHour, requiredRecordFields, resolveBreakdownLive, resolveTotalsLive, runHourlyAthenaQuery, shiftAnchor, sumBreakdown, sumTotals, topNWithOther, totalsSchema, windowKeys, yearKey };
|
package/dist/index.js
CHANGED
|
@@ -837,6 +837,7 @@ function toBreakdownRow(item) {
|
|
|
837
837
|
return { value: item.value, hits: item.hits, bytes: item.bytes };
|
|
838
838
|
}
|
|
839
839
|
// packages/analytics-processor/src/resolveLive.ts
|
|
840
|
+
var bucketStates = ["sealed", "partial", "pending"];
|
|
840
841
|
var emptyTotals = {
|
|
841
842
|
hits: 0,
|
|
842
843
|
success: 0,
|
|
@@ -870,21 +871,22 @@ async function resolveTotalsLive({
|
|
|
870
871
|
});
|
|
871
872
|
return sumTotals(hours);
|
|
872
873
|
}
|
|
873
|
-
const
|
|
874
|
-
const sealedDays = await models.totals.queryWindow({
|
|
874
|
+
const storedDays = await models.totals.queryWindow({
|
|
875
875
|
grain: "day",
|
|
876
876
|
startKey: `${periodKey}-01`,
|
|
877
877
|
endKey: `${periodKey}-31`
|
|
878
878
|
});
|
|
879
|
-
const
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
879
|
+
const base = sumTotals(storedDays.filter((item) => !isCurrentPeriod({ grain: "day", periodKey: item.timeKey, now })));
|
|
880
|
+
const currentDayKey = currentChildKey({ parentGrain: "month", periodKey, now });
|
|
881
|
+
if (currentDayKey == null)
|
|
882
|
+
return base;
|
|
883
|
+
const currentDay = await resolveTotalsLive({
|
|
884
|
+
models,
|
|
885
|
+
grain: "day",
|
|
886
|
+
periodKey: currentDayKey,
|
|
887
|
+
now
|
|
888
|
+
});
|
|
889
|
+
return sumTotals([base, currentDay]);
|
|
888
890
|
}
|
|
889
891
|
async function resolveBreakdownLive({
|
|
890
892
|
models,
|
|
@@ -901,10 +903,39 @@ async function resolveBreakdownLive({
|
|
|
901
903
|
if (stored.length > 0)
|
|
902
904
|
return toRows(stored);
|
|
903
905
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
906
|
+
if (grain === "day") {
|
|
907
|
+
const hourKeys = hourKeysInProgress({ periodKey, now });
|
|
908
|
+
const hourRows = await Promise.all(hourKeys.map((hourKey) => models.breakdowns.getForPeriod({ dimension, grain: "hour", periodKey: hourKey })));
|
|
909
|
+
return sumBreakdown(hourRows.flat().map(toRow));
|
|
910
|
+
}
|
|
911
|
+
const storedDayKeys = pastDayKeys({ periodKey, now });
|
|
912
|
+
const storedDayRows = await Promise.all(storedDayKeys.map((dayKey) => models.breakdowns.getForPeriod({ dimension, grain: "day", periodKey: dayKey })));
|
|
913
|
+
const rows = storedDayRows.flat().map(toRow);
|
|
914
|
+
const currentDayKey = currentChildKey({ parentGrain: "month", periodKey, now });
|
|
915
|
+
if (currentDayKey != null) {
|
|
916
|
+
const currentDay = await resolveBreakdownLive({
|
|
917
|
+
models,
|
|
918
|
+
dimension,
|
|
919
|
+
grain: "day",
|
|
920
|
+
periodKey: currentDayKey,
|
|
921
|
+
now
|
|
922
|
+
});
|
|
923
|
+
rows.push(...currentDay);
|
|
924
|
+
}
|
|
925
|
+
return sumBreakdown(rows);
|
|
926
|
+
}
|
|
927
|
+
async function classifyBucketState({
|
|
928
|
+
models,
|
|
929
|
+
grain,
|
|
930
|
+
periodKey,
|
|
931
|
+
now,
|
|
932
|
+
hasData
|
|
933
|
+
}) {
|
|
934
|
+
if (await isSealed({ models, grain, periodKey, now }))
|
|
935
|
+
return "sealed";
|
|
936
|
+
if (hasData)
|
|
937
|
+
return "partial";
|
|
938
|
+
return "pending";
|
|
908
939
|
}
|
|
909
940
|
async function isSealed({
|
|
910
941
|
models,
|
|
@@ -912,7 +943,7 @@ async function isSealed({
|
|
|
912
943
|
periodKey,
|
|
913
944
|
now
|
|
914
945
|
}) {
|
|
915
|
-
if (
|
|
946
|
+
if (!isPastPeriod({ grain, periodKey, now }))
|
|
916
947
|
return false;
|
|
917
948
|
return models.markers.isApplied({ grain, periodKey });
|
|
918
949
|
}
|
|
@@ -923,6 +954,13 @@ function isCurrentPeriod({
|
|
|
923
954
|
}) {
|
|
924
955
|
return periodKey === currentKeyFor({ grain, now });
|
|
925
956
|
}
|
|
957
|
+
function isPastPeriod({
|
|
958
|
+
grain,
|
|
959
|
+
periodKey,
|
|
960
|
+
now
|
|
961
|
+
}) {
|
|
962
|
+
return periodKey < currentKeyFor({ grain, now });
|
|
963
|
+
}
|
|
926
964
|
function currentKeyFor({ grain, now }) {
|
|
927
965
|
const year = pad4(now.getUTCFullYear());
|
|
928
966
|
const month = pad2(now.getUTCMonth() + 1);
|
|
@@ -934,8 +972,14 @@ function currentKeyFor({ grain, now }) {
|
|
|
934
972
|
return `${year}-${month}-${day}`;
|
|
935
973
|
return `${year}-${month}-${day}-${hour}`;
|
|
936
974
|
}
|
|
937
|
-
function
|
|
938
|
-
|
|
975
|
+
function currentChildKey({
|
|
976
|
+
parentGrain,
|
|
977
|
+
periodKey,
|
|
978
|
+
now
|
|
979
|
+
}) {
|
|
980
|
+
if (!isCurrentPeriod({ grain: parentGrain, periodKey, now }))
|
|
981
|
+
return null;
|
|
982
|
+
return currentKeyFor({ grain: "day", now });
|
|
939
983
|
}
|
|
940
984
|
function hourKeysInProgress({
|
|
941
985
|
periodKey,
|
|
@@ -948,11 +992,12 @@ function hourKeysInProgress({
|
|
|
948
992
|
}
|
|
949
993
|
return keys;
|
|
950
994
|
}
|
|
951
|
-
function
|
|
995
|
+
function pastDayKeys({
|
|
952
996
|
periodKey,
|
|
953
997
|
now
|
|
954
998
|
}) {
|
|
955
|
-
const
|
|
999
|
+
const isCurrentMonth = isCurrentPeriod({ grain: "month", periodKey, now });
|
|
1000
|
+
const lastDay = isCurrentMonth ? now.getUTCDate() - 1 : daysInMonth(periodKey);
|
|
956
1001
|
const keys = [];
|
|
957
1002
|
for (let day = 1;day <= lastDay; day += 1) {
|
|
958
1003
|
keys.push(`${periodKey}-${pad2(day)}`);
|
|
@@ -963,15 +1008,11 @@ function daysInMonth(monthKey) {
|
|
|
963
1008
|
const [year, month] = monthKey.split("-").map(Number);
|
|
964
1009
|
return new Date(Date.UTC(year ?? 1970, month ?? 1, 0)).getUTCDate();
|
|
965
1010
|
}
|
|
966
|
-
function
|
|
967
|
-
|
|
968
|
-
for (const item of items) {
|
|
969
|
-
byKey[item.timeKey] = item;
|
|
970
|
-
}
|
|
971
|
-
return byKey;
|
|
1011
|
+
function toRow(item) {
|
|
1012
|
+
return { value: item.value, hits: item.hits, bytes: item.bytes };
|
|
972
1013
|
}
|
|
973
1014
|
function toRows(items) {
|
|
974
|
-
return items.map(
|
|
1015
|
+
return items.map(toRow);
|
|
975
1016
|
}
|
|
976
1017
|
function pickTotals(item) {
|
|
977
1018
|
return {
|
|
@@ -1223,8 +1264,10 @@ export {
|
|
|
1223
1264
|
applyDailyRollup,
|
|
1224
1265
|
applyMonthlyRollup,
|
|
1225
1266
|
breakdownSchema,
|
|
1267
|
+
bucketStates,
|
|
1226
1268
|
buildHourlyQuery,
|
|
1227
1269
|
chunkTransactItems,
|
|
1270
|
+
classifyBucketState,
|
|
1228
1271
|
classifyDevice,
|
|
1229
1272
|
columnMaps,
|
|
1230
1273
|
configSchema,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beesolve/analytics-processor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CloudFront analytics processor — Athena-to-DynamoDB hourly totals, breakdowns, and rollups",
|
|
5
5
|
"homepage": "https://github.com/BeeSolve/packages/tree/main/packages/analytics-processor#readme",
|
|
6
6
|
"license": "MIT",
|