@contractspec/lib.metering 3.7.17 → 3.7.19

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 (34) hide show
  1. package/dist/aggregation/index.js +1 -265
  2. package/dist/analytics/posthog-metering-reader.js +4 -266
  3. package/dist/analytics/posthog-metering.js +1 -45
  4. package/dist/browser/aggregation/index.js +1 -265
  5. package/dist/browser/analytics/posthog-metering-reader.js +4 -266
  6. package/dist/browser/analytics/posthog-metering.js +1 -45
  7. package/dist/browser/contracts/index.js +1 -617
  8. package/dist/browser/docs/index.js +4 -18
  9. package/dist/browser/docs/metering.docblock.js +4 -18
  10. package/dist/browser/entities/index.js +1 -350
  11. package/dist/browser/events.js +1 -269
  12. package/dist/browser/index.js +7 -1877
  13. package/dist/browser/metering.capability.js +1 -31
  14. package/dist/browser/metering.feature.js +1 -53
  15. package/dist/contracts/index.js +1 -617
  16. package/dist/docs/index.js +4 -18
  17. package/dist/docs/metering.docblock.js +4 -18
  18. package/dist/entities/index.js +1 -350
  19. package/dist/events.js +1 -269
  20. package/dist/index.js +7 -1877
  21. package/dist/metering.capability.js +1 -31
  22. package/dist/metering.feature.js +1 -53
  23. package/dist/node/aggregation/index.js +1 -265
  24. package/dist/node/analytics/posthog-metering-reader.js +4 -266
  25. package/dist/node/analytics/posthog-metering.js +1 -45
  26. package/dist/node/contracts/index.js +1 -617
  27. package/dist/node/docs/index.js +4 -18
  28. package/dist/node/docs/metering.docblock.js +4 -18
  29. package/dist/node/entities/index.js +1 -350
  30. package/dist/node/events.js +1 -269
  31. package/dist/node/index.js +7 -1877
  32. package/dist/node/metering.capability.js +1 -31
  33. package/dist/node/metering.feature.js +1 -53
  34. package/package.json +6 -6
@@ -1,32 +1,2 @@
1
1
  // @bun
2
- // src/metering.capability.ts
3
- import {
4
- defineCapability,
5
- StabilityEnum
6
- } from "@contractspec/lib.contracts-spec";
7
- var MeteringCapability = defineCapability({
8
- meta: {
9
- key: "metering",
10
- version: "1.0.0",
11
- kind: "api",
12
- stability: StabilityEnum.Experimental,
13
- description: "Usage metering and tracking",
14
- owners: ["@platform.finance"],
15
- tags: ["metering", "usage", "billing"]
16
- }
17
- });
18
- var ThresholdsCapability = defineCapability({
19
- meta: {
20
- key: "thresholds",
21
- version: "1.0.0",
22
- kind: "api",
23
- stability: StabilityEnum.Experimental,
24
- description: "Usage threshold alerts and limits",
25
- owners: ["@platform.finance"],
26
- tags: ["thresholds", "limits", "metering"]
27
- }
28
- });
29
- export {
30
- ThresholdsCapability,
31
- MeteringCapability
32
- };
2
+ import{defineCapability as g,StabilityEnum as h}from"@contractspec/lib.contracts-spec";var k=g({meta:{key:"metering",version:"1.0.0",kind:"api",stability:h.Experimental,description:"Usage metering and tracking",owners:["@platform.finance"],tags:["metering","usage","billing"]}}),q=g({meta:{key:"thresholds",version:"1.0.0",kind:"api",stability:h.Experimental,description:"Usage threshold alerts and limits",owners:["@platform.finance"],tags:["thresholds","limits","metering"]}});export{q as ThresholdsCapability,k as MeteringCapability};
@@ -1,54 +1,2 @@
1
1
  // @bun
2
- // src/metering.feature.ts
3
- import { defineFeature } from "@contractspec/lib.contracts-spec";
4
- var MeteringFeature = defineFeature({
5
- meta: {
6
- key: "metrics",
7
- version: "1.0.0",
8
- title: "Usage Metering",
9
- description: "Usage metering, metric definitions, and threshold alerting",
10
- domain: "platform",
11
- owners: ["@platform.metering"],
12
- tags: ["metering", "usage", "billing", "thresholds"],
13
- stability: "stable"
14
- },
15
- operations: [
16
- { key: "metric.define", version: "1.0.0" },
17
- { key: "metric.update", version: "1.0.0" },
18
- { key: "metric.delete", version: "1.0.0" },
19
- { key: "metric.get", version: "1.0.0" },
20
- { key: "metric.list", version: "1.0.0" },
21
- { key: "usage.record", version: "1.0.0" },
22
- { key: "usage.recordBatch", version: "1.0.0" },
23
- { key: "usage.get", version: "1.0.0" },
24
- { key: "usage.getSummary", version: "1.0.0" },
25
- { key: "threshold.create", version: "1.0.0" },
26
- { key: "threshold.update", version: "1.0.0" },
27
- { key: "threshold.delete", version: "1.0.0" },
28
- { key: "threshold.list", version: "1.0.0" }
29
- ],
30
- events: [
31
- { key: "metric.defined", version: "1.0.0" },
32
- { key: "metric.updated", version: "1.0.0" },
33
- { key: "usage.recorded", version: "1.0.0" },
34
- { key: "usage.batch_recorded", version: "1.0.0" },
35
- { key: "usage.aggregated", version: "1.0.0" },
36
- { key: "threshold.created", version: "1.0.0" },
37
- { key: "threshold.exceeded", version: "1.0.0" },
38
- { key: "threshold.approaching", version: "1.0.0" },
39
- { key: "model.selected", version: "1.0.0" }
40
- ],
41
- presentations: [],
42
- opToPresentation: [],
43
- presentationsTargets: [],
44
- capabilities: {
45
- provides: [
46
- { key: "metering", version: "1.0.0" },
47
- { key: "thresholds", version: "1.0.0" }
48
- ],
49
- requires: []
50
- }
51
- });
52
- export {
53
- MeteringFeature
54
- };
2
+ import{defineFeature as g}from"@contractspec/lib.contracts-spec";var j=g({meta:{key:"metrics",version:"1.0.0",title:"Usage Metering",description:"Usage metering, metric definitions, and threshold alerting",domain:"platform",owners:["@platform.metering"],tags:["metering","usage","billing","thresholds"],stability:"stable"},operations:[{key:"metric.define",version:"1.0.0"},{key:"metric.update",version:"1.0.0"},{key:"metric.delete",version:"1.0.0"},{key:"metric.get",version:"1.0.0"},{key:"metric.list",version:"1.0.0"},{key:"usage.record",version:"1.0.0"},{key:"usage.recordBatch",version:"1.0.0"},{key:"usage.get",version:"1.0.0"},{key:"usage.getSummary",version:"1.0.0"},{key:"threshold.create",version:"1.0.0"},{key:"threshold.update",version:"1.0.0"},{key:"threshold.delete",version:"1.0.0"},{key:"threshold.list",version:"1.0.0"}],events:[{key:"metric.defined",version:"1.0.0"},{key:"metric.updated",version:"1.0.0"},{key:"usage.recorded",version:"1.0.0"},{key:"usage.batch_recorded",version:"1.0.0"},{key:"usage.aggregated",version:"1.0.0"},{key:"threshold.created",version:"1.0.0"},{key:"threshold.exceeded",version:"1.0.0"},{key:"threshold.approaching",version:"1.0.0"},{key:"model.selected",version:"1.0.0"}],presentations:[],opToPresentation:[],presentationsTargets:[],capabilities:{provides:[{key:"metering",version:"1.0.0"},{key:"thresholds",version:"1.0.0"}],requires:[]}});export{j as MeteringFeature};
@@ -1,265 +1 @@
1
- // src/aggregation/index.ts
2
- function getPeriodStart(date, periodType) {
3
- const d = new Date(date);
4
- switch (periodType) {
5
- case "HOURLY":
6
- d.setMinutes(0, 0, 0);
7
- return d;
8
- case "DAILY":
9
- d.setHours(0, 0, 0, 0);
10
- return d;
11
- case "WEEKLY": {
12
- d.setHours(0, 0, 0, 0);
13
- const day = d.getDay();
14
- d.setDate(d.getDate() - day);
15
- return d;
16
- }
17
- case "MONTHLY":
18
- d.setHours(0, 0, 0, 0);
19
- d.setDate(1);
20
- return d;
21
- case "YEARLY":
22
- d.setHours(0, 0, 0, 0);
23
- d.setMonth(0, 1);
24
- return d;
25
- }
26
- }
27
- function getPeriodEnd(date, periodType) {
28
- const start = getPeriodStart(date, periodType);
29
- switch (periodType) {
30
- case "HOURLY":
31
- return new Date(start.getTime() + 60 * 60 * 1000);
32
- case "DAILY":
33
- return new Date(start.getTime() + 24 * 60 * 60 * 1000);
34
- case "WEEKLY":
35
- return new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);
36
- case "MONTHLY": {
37
- const end = new Date(start);
38
- end.setMonth(end.getMonth() + 1);
39
- return end;
40
- }
41
- case "YEARLY": {
42
- const end = new Date(start);
43
- end.setFullYear(end.getFullYear() + 1);
44
- return end;
45
- }
46
- }
47
- }
48
- function formatPeriodKey(date, periodType) {
49
- const start = getPeriodStart(date, periodType);
50
- const year = start.getFullYear();
51
- const month = String(start.getMonth() + 1).padStart(2, "0");
52
- const day = String(start.getDate()).padStart(2, "0");
53
- const hour = String(start.getHours()).padStart(2, "0");
54
- switch (periodType) {
55
- case "HOURLY":
56
- return `${year}-${month}-${day}T${hour}`;
57
- case "DAILY":
58
- return `${year}-${month}-${day}`;
59
- case "WEEKLY":
60
- return `${year}-W${getWeekNumber(start)}`;
61
- case "MONTHLY":
62
- return `${year}-${month}`;
63
- case "YEARLY":
64
- return `${year}`;
65
- }
66
- }
67
- function getWeekNumber(date) {
68
- const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
69
- const dayNum = d.getUTCDay() || 7;
70
- d.setUTCDate(d.getUTCDate() + 4 - dayNum);
71
- const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
72
- const weekNum = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
73
- return String(weekNum).padStart(2, "0");
74
- }
75
-
76
- class UsageAggregator {
77
- storage;
78
- batchSize;
79
- constructor(options) {
80
- this.storage = options.storage;
81
- this.batchSize = options.batchSize || 1000;
82
- }
83
- async aggregate(params) {
84
- const { periodType, periodStart, metricKey } = params;
85
- const periodEnd = params.periodEnd || getPeriodEnd(periodStart, periodType);
86
- const result = {
87
- periodType,
88
- periodStart,
89
- periodEnd,
90
- recordsProcessed: 0,
91
- summariesCreated: 0,
92
- summariesUpdated: 0,
93
- errors: []
94
- };
95
- const records = await this.storage.getUnaggregatedRecords({
96
- metricKey,
97
- periodStart,
98
- periodEnd,
99
- limit: this.batchSize
100
- });
101
- if (records.length === 0) {
102
- return result;
103
- }
104
- const groups = this.groupRecords(records, periodType);
105
- for (const [groupKey, groupRecords] of groups.entries()) {
106
- try {
107
- await this.aggregateGroup(groupKey, groupRecords, periodType, result);
108
- } catch (error) {
109
- const [metricKey2, subjectType, subjectId] = groupKey.split("::");
110
- result.errors.push({
111
- metricKey: metricKey2 ?? "unknown",
112
- subjectType: subjectType ?? "unknown",
113
- subjectId: subjectId ?? "unknown",
114
- error: error instanceof Error ? error.message : String(error)
115
- });
116
- }
117
- }
118
- const recordIds = records.map((r) => r.id);
119
- await this.storage.markRecordsAggregated(recordIds, new Date);
120
- result.recordsProcessed = records.length;
121
- return result;
122
- }
123
- groupRecords(records, periodType) {
124
- const groups = new Map;
125
- for (const record of records) {
126
- const periodKey = formatPeriodKey(record.timestamp, periodType);
127
- const groupKey = `${record.metricKey}::${record.subjectType}::${record.subjectId}::${periodKey}`;
128
- const existing = groups.get(groupKey) || [];
129
- existing.push(record);
130
- groups.set(groupKey, existing);
131
- }
132
- return groups;
133
- }
134
- async aggregateGroup(groupKey, records, periodType, result) {
135
- const [metricKey, subjectType, subjectId] = groupKey.split("::");
136
- if (!metricKey || !subjectType || !subjectId || records.length === 0) {
137
- return;
138
- }
139
- const firstRecord = records[0];
140
- if (!firstRecord)
141
- return;
142
- const periodStart = getPeriodStart(firstRecord.timestamp, periodType);
143
- const periodEnd = getPeriodEnd(firstRecord.timestamp, periodType);
144
- const metric = await this.storage.getMetric(metricKey);
145
- const aggregationType = metric?.aggregationType || "SUM";
146
- const quantities = records.map((r) => r.quantity);
147
- const aggregated = this.calculateAggregation(quantities, aggregationType);
148
- await this.storage.upsertSummary({
149
- metricKey,
150
- subjectType,
151
- subjectId,
152
- periodType,
153
- periodStart,
154
- periodEnd,
155
- totalQuantity: aggregated.total,
156
- recordCount: records.length,
157
- minQuantity: aggregated.min,
158
- maxQuantity: aggregated.max,
159
- avgQuantity: aggregated.avg
160
- });
161
- result.summariesCreated++;
162
- }
163
- calculateAggregation(quantities, aggregationType) {
164
- if (quantities.length === 0) {
165
- return { total: 0, min: 0, max: 0, avg: 0 };
166
- }
167
- const min = Math.min(...quantities);
168
- const max = Math.max(...quantities);
169
- const sum = quantities.reduce((a, b) => a + b, 0);
170
- const avg = sum / quantities.length;
171
- const count = quantities.length;
172
- let total;
173
- switch (aggregationType) {
174
- case "COUNT":
175
- total = count;
176
- break;
177
- case "SUM":
178
- total = sum;
179
- break;
180
- case "AVG":
181
- total = avg;
182
- break;
183
- case "MAX":
184
- total = max;
185
- break;
186
- case "MIN":
187
- total = min;
188
- break;
189
- case "LAST":
190
- total = quantities[quantities.length - 1] ?? 0;
191
- break;
192
- default:
193
- total = sum;
194
- }
195
- return { total, min, max, avg };
196
- }
197
- }
198
-
199
- class InMemoryUsageStorage {
200
- records = [];
201
- summaries = new Map;
202
- metrics = new Map;
203
- addRecord(record) {
204
- this.records.push(record);
205
- }
206
- addMetric(metric) {
207
- this.metrics.set(metric.key, metric);
208
- }
209
- async getUnaggregatedRecords(options) {
210
- let records = this.records.filter((r) => {
211
- const inPeriod = r.timestamp >= options.periodStart && r.timestamp < options.periodEnd;
212
- const matchesMetric = !options.metricKey || r.metricKey === options.metricKey;
213
- return inPeriod && matchesMetric;
214
- });
215
- if (options.limit) {
216
- records = records.slice(0, options.limit);
217
- }
218
- return records;
219
- }
220
- async markRecordsAggregated(recordIds) {
221
- this.records = this.records.filter((r) => !recordIds.includes(r.id));
222
- }
223
- async upsertSummary(summary) {
224
- const key = `${summary.metricKey}::${summary.subjectType}::${summary.subjectId}::${summary.periodType}::${summary.periodStart.toISOString()}`;
225
- const existing = this.summaries.get(key);
226
- if (existing) {
227
- existing.totalQuantity += summary.totalQuantity;
228
- existing.recordCount += summary.recordCount;
229
- if (summary.minQuantity !== undefined) {
230
- existing.minQuantity = Math.min(existing.minQuantity ?? Infinity, summary.minQuantity);
231
- }
232
- if (summary.maxQuantity !== undefined) {
233
- existing.maxQuantity = Math.max(existing.maxQuantity ?? -Infinity, summary.maxQuantity);
234
- }
235
- return existing;
236
- }
237
- const newSummary = {
238
- id: `summary-${Date.now()}-${Math.random().toString(36).slice(2)}`,
239
- ...summary
240
- };
241
- this.summaries.set(key, newSummary);
242
- return newSummary;
243
- }
244
- async getMetric(key) {
245
- return this.metrics.get(key) || null;
246
- }
247
- async listMetrics() {
248
- return Array.from(this.metrics.values());
249
- }
250
- getSummaries() {
251
- return Array.from(this.summaries.values());
252
- }
253
- clear() {
254
- this.records = [];
255
- this.summaries.clear();
256
- this.metrics.clear();
257
- }
258
- }
259
- export {
260
- getPeriodStart,
261
- getPeriodEnd,
262
- formatPeriodKey,
263
- UsageAggregator,
264
- InMemoryUsageStorage
265
- };
1
+ function Z(z,B){let A=new Date(z);switch(B){case"HOURLY":return A.setMinutes(0,0,0),A;case"DAILY":return A.setHours(0,0,0,0),A;case"WEEKLY":{A.setHours(0,0,0,0);let D=A.getDay();return A.setDate(A.getDate()-D),A}case"MONTHLY":return A.setHours(0,0,0,0),A.setDate(1),A;case"YEARLY":return A.setHours(0,0,0,0),A.setMonth(0,1),A}}function $(z,B){let A=Z(z,B);switch(B){case"HOURLY":return new Date(A.getTime()+3600000);case"DAILY":return new Date(A.getTime()+86400000);case"WEEKLY":return new Date(A.getTime()+604800000);case"MONTHLY":{let D=new Date(A);return D.setMonth(D.getMonth()+1),D}case"YEARLY":{let D=new Date(A);return D.setFullYear(D.getFullYear()+1),D}}}function C(z,B){let A=Z(z,B),D=A.getFullYear(),F=String(A.getMonth()+1).padStart(2,"0"),G=String(A.getDate()).padStart(2,"0"),H=String(A.getHours()).padStart(2,"0");switch(B){case"HOURLY":return`${D}-${F}-${G}T${H}`;case"DAILY":return`${D}-${F}-${G}`;case"WEEKLY":return`${D}-W${M(A)}`;case"MONTHLY":return`${D}-${F}`;case"YEARLY":return`${D}`}}function M(z){let B=new Date(Date.UTC(z.getFullYear(),z.getMonth(),z.getDate())),A=B.getUTCDay()||7;B.setUTCDate(B.getUTCDate()+4-A);let D=new Date(Date.UTC(B.getUTCFullYear(),0,1)),F=Math.ceil(((B.getTime()-D.getTime())/86400000+1)/7);return String(F).padStart(2,"0")}class U{storage;batchSize;constructor(z){this.storage=z.storage,this.batchSize=z.batchSize||1000}async aggregate(z){let{periodType:B,periodStart:A,metricKey:D}=z,F=z.periodEnd||$(A,B),G={periodType:B,periodStart:A,periodEnd:F,recordsProcessed:0,summariesCreated:0,summariesUpdated:0,errors:[]},H=await this.storage.getUnaggregatedRecords({metricKey:D,periodStart:A,periodEnd:F,limit:this.batchSize});if(H.length===0)return G;let J=this.groupRecords(H,B);for(let[L,_]of J.entries())try{await this.aggregateGroup(L,_,B,G)}catch(V){let[X,O,Y]=L.split("::");G.errors.push({metricKey:X??"unknown",subjectType:O??"unknown",subjectId:Y??"unknown",error:V instanceof Error?V.message:String(V)})}let Q=H.map((L)=>L.id);return await this.storage.markRecordsAggregated(Q,new Date),G.recordsProcessed=H.length,G}groupRecords(z,B){let A=new Map;for(let D of z){let F=C(D.timestamp,B),G=`${D.metricKey}::${D.subjectType}::${D.subjectId}::${F}`,H=A.get(G)||[];H.push(D),A.set(G,H)}return A}async aggregateGroup(z,B,A,D){let[F,G,H]=z.split("::");if(!F||!G||!H||B.length===0)return;let J=B[0];if(!J)return;let Q=Z(J.timestamp,A),L=$(J.timestamp,A),V=(await this.storage.getMetric(F))?.aggregationType||"SUM",X=B.map((Y)=>Y.quantity),O=this.calculateAggregation(X,V);await this.storage.upsertSummary({metricKey:F,subjectType:G,subjectId:H,periodType:A,periodStart:Q,periodEnd:L,totalQuantity:O.total,recordCount:B.length,minQuantity:O.min,maxQuantity:O.max,avgQuantity:O.avg}),D.summariesCreated++}calculateAggregation(z,B){if(z.length===0)return{total:0,min:0,max:0,avg:0};let A=Math.min(...z),D=Math.max(...z),F=z.reduce((Q,L)=>Q+L,0),G=F/z.length,H=z.length,J;switch(B){case"COUNT":J=H;break;case"SUM":J=F;break;case"AVG":J=G;break;case"MAX":J=D;break;case"MIN":J=A;break;case"LAST":J=z[z.length-1]??0;break;default:J=F}return{total:J,min:A,max:D,avg:G}}}class W{records=[];summaries=new Map;metrics=new Map;addRecord(z){this.records.push(z)}addMetric(z){this.metrics.set(z.key,z)}async getUnaggregatedRecords(z){let B=this.records.filter((A)=>{let D=A.timestamp>=z.periodStart&&A.timestamp<z.periodEnd,F=!z.metricKey||A.metricKey===z.metricKey;return D&&F});if(z.limit)B=B.slice(0,z.limit);return B}async markRecordsAggregated(z){this.records=this.records.filter((B)=>!z.includes(B.id))}async upsertSummary(z){let B=`${z.metricKey}::${z.subjectType}::${z.subjectId}::${z.periodType}::${z.periodStart.toISOString()}`,A=this.summaries.get(B);if(A){if(A.totalQuantity+=z.totalQuantity,A.recordCount+=z.recordCount,z.minQuantity!==void 0)A.minQuantity=Math.min(A.minQuantity??1/0,z.minQuantity);if(z.maxQuantity!==void 0)A.maxQuantity=Math.max(A.maxQuantity??-1/0,z.maxQuantity);return A}let D={id:`summary-${Date.now()}-${Math.random().toString(36).slice(2)}`,...z};return this.summaries.set(B,D),D}async getMetric(z){return this.metrics.get(z)||null}async listMetrics(){return Array.from(this.metrics.values())}getSummaries(){return Array.from(this.summaries.values())}clear(){this.records=[],this.summaries.clear(),this.metrics.clear()}}export{Z as getPeriodStart,$ as getPeriodEnd,C as formatPeriodKey,U as UsageAggregator,W as InMemoryUsageStorage};
@@ -1,266 +1,4 @@
1
- // src/analytics/posthog-metering-reader.ts
2
- class PosthogMeteringReader {
3
- reader;
4
- eventPrefix;
5
- constructor(reader, options = {}) {
6
- this.reader = reader;
7
- this.eventPrefix = options.eventPrefix ?? "metering";
8
- }
9
- async getUsageByMetric(input) {
10
- const result = await this.queryHogQL({
11
- query: [
12
- "select",
13
- " properties.recordId as recordId,",
14
- " properties.metricKey as metricKey,",
15
- " properties.subjectType as subjectType,",
16
- " distinct_id as subjectId,",
17
- " properties.quantity as quantity,",
18
- " properties.source as source,",
19
- " timestamp as timestamp",
20
- "from events",
21
- `where ${buildUsageWhereClause(this.eventPrefix, input)}`,
22
- "order by timestamp desc",
23
- `limit ${input.limit ?? 200}`
24
- ].join(`
25
- `),
26
- values: buildUsageValues(input)
27
- });
28
- return mapUsageRecords(result);
29
- }
30
- async getUsageSummary(input) {
31
- const result = await this.queryHogQL({
32
- query: [
33
- "select",
34
- " properties.summaryId as summaryId,",
35
- " properties.metricKey as metricKey,",
36
- " properties.subjectType as subjectType,",
37
- " distinct_id as subjectId,",
38
- " properties.periodType as periodType,",
39
- " properties.periodStart as periodStart,",
40
- " properties.periodEnd as periodEnd,",
41
- " properties.totalQuantity as totalQuantity,",
42
- " properties.recordCount as recordCount,",
43
- " timestamp as aggregatedAt",
44
- "from events",
45
- `where ${buildSummaryWhereClause(this.eventPrefix, input)}`,
46
- "order by timestamp desc",
47
- `limit ${input.limit ?? 200}`
48
- ].join(`
49
- `),
50
- values: buildSummaryValues(input)
51
- });
52
- return mapUsageSummaries(result);
53
- }
54
- async getUsageTrend(input) {
55
- const result = await this.queryHogQL({
56
- query: [
57
- "select",
58
- ` ${bucketExpression(input.bucket)} as bucketStart,`,
59
- " sum(properties.quantity) as totalQuantity,",
60
- " count() as recordCount",
61
- "from events",
62
- `where ${buildUsageWhereClause(this.eventPrefix, input)}`,
63
- "group by bucketStart",
64
- "order by bucketStart asc"
65
- ].join(`
66
- `),
67
- values: buildUsageValues(input)
68
- });
69
- return mapUsageTrend(result);
70
- }
71
- async queryHogQL(input) {
72
- if (!this.reader.queryHogQL) {
73
- throw new Error("Analytics reader does not support HogQL queries.");
74
- }
75
- return this.reader.queryHogQL(input);
76
- }
77
- }
78
- function buildUsageWhereClause(eventPrefix, input) {
79
- const clauses = [
80
- `event = '${eventPrefix}.usage_recorded'`,
81
- `properties.metricKey = {metricKey}`
82
- ];
83
- if (input.subjectId) {
84
- clauses.push("distinct_id = {subjectId}");
85
- }
86
- if (input.dateRange?.from) {
87
- clauses.push("timestamp >= {dateFrom}");
88
- }
89
- if (input.dateRange?.to) {
90
- clauses.push("timestamp < {dateTo}");
91
- }
92
- return clauses.join(" and ");
93
- }
94
- function buildSummaryWhereClause(eventPrefix, input) {
95
- const clauses = [
96
- `event = '${eventPrefix}.usage_aggregated'`,
97
- `properties.metricKey = {metricKey}`
98
- ];
99
- if (input.subjectId) {
100
- clauses.push("distinct_id = {subjectId}");
101
- }
102
- if (input.periodType) {
103
- clauses.push("properties.periodType = {periodType}");
104
- }
105
- if (input.dateRange?.from) {
106
- clauses.push("timestamp >= {dateFrom}");
107
- }
108
- if (input.dateRange?.to) {
109
- clauses.push("timestamp < {dateTo}");
110
- }
111
- return clauses.join(" and ");
112
- }
113
- function buildUsageValues(input) {
114
- return {
115
- metricKey: input.metricKey,
116
- subjectId: input.subjectId,
117
- dateFrom: toIsoString(input.dateRange?.from),
118
- dateTo: toIsoString(input.dateRange?.to)
119
- };
120
- }
121
- function buildSummaryValues(input) {
122
- return {
123
- metricKey: input.metricKey,
124
- subjectId: input.subjectId,
125
- periodType: input.periodType,
126
- dateFrom: toIsoString(input.dateRange?.from),
127
- dateTo: toIsoString(input.dateRange?.to)
128
- };
129
- }
130
- function bucketExpression(bucket) {
131
- switch (bucket) {
132
- case "hour":
133
- return "toStartOfHour(timestamp)";
134
- case "week":
135
- return "toStartOfWeek(timestamp)";
136
- case "month":
137
- return "toStartOfMonth(timestamp)";
138
- case "day":
139
- default:
140
- return "toStartOfDay(timestamp)";
141
- }
142
- }
143
- function mapUsageRecords(result) {
144
- const rows = mapRows(result);
145
- return rows.flatMap((row) => {
146
- const metricKey = asString(row.metricKey);
147
- const subjectType = asString(row.subjectType);
148
- const subjectId = asString(row.subjectId);
149
- const timestamp = asDate(row.timestamp);
150
- if (!metricKey || !subjectType || !subjectId || !timestamp) {
151
- return [];
152
- }
153
- return [
154
- {
155
- recordId: asOptionalString(row.recordId),
156
- metricKey,
157
- subjectType,
158
- subjectId,
159
- quantity: asNumber(row.quantity),
160
- source: asOptionalString(row.source),
161
- timestamp
162
- }
163
- ];
164
- });
165
- }
166
- function mapUsageSummaries(result) {
167
- const rows = mapRows(result);
168
- return rows.flatMap((row) => {
169
- const metricKey = asString(row.metricKey);
170
- const subjectType = asString(row.subjectType);
171
- const subjectId = asString(row.subjectId);
172
- const periodType = asString(row.periodType);
173
- const periodStart = asDate(row.periodStart);
174
- const periodEnd = asDate(row.periodEnd);
175
- const aggregatedAt = asDate(row.aggregatedAt);
176
- if (!metricKey || !subjectType || !subjectId || !periodType || !periodStart || !periodEnd || !aggregatedAt) {
177
- return [];
178
- }
179
- return [
180
- {
181
- summaryId: asOptionalString(row.summaryId),
182
- metricKey,
183
- subjectType,
184
- subjectId,
185
- periodType,
186
- periodStart,
187
- periodEnd,
188
- totalQuantity: asNumber(row.totalQuantity),
189
- recordCount: asNumber(row.recordCount),
190
- aggregatedAt
191
- }
192
- ];
193
- });
194
- }
195
- function mapUsageTrend(result) {
196
- const rows = mapRows(result);
197
- return rows.flatMap((row) => {
198
- const bucketStart = asString(row.bucketStart);
199
- if (!bucketStart)
200
- return [];
201
- return [
202
- {
203
- bucketStart,
204
- totalQuantity: asNumber(row.totalQuantity),
205
- recordCount: asNumber(row.recordCount)
206
- }
207
- ];
208
- });
209
- }
210
- function mapRows(result) {
211
- if (!Array.isArray(result.results) || !Array.isArray(result.columns)) {
212
- return [];
213
- }
214
- const columns = result.columns;
215
- return result.results.flatMap((row) => {
216
- if (!Array.isArray(row))
217
- return [];
218
- const record = {};
219
- columns.forEach((column, index) => {
220
- record[column] = row[index];
221
- });
222
- return [record];
223
- });
224
- }
225
- function asString(value) {
226
- if (typeof value === "string" && value.trim())
227
- return value;
228
- if (typeof value === "number")
229
- return String(value);
230
- return null;
231
- }
232
- function asOptionalString(value) {
233
- if (typeof value === "string")
234
- return value;
235
- if (typeof value === "number")
236
- return String(value);
237
- return;
238
- }
239
- function asNumber(value) {
240
- if (typeof value === "number" && Number.isFinite(value))
241
- return value;
242
- if (typeof value === "string" && value.trim()) {
243
- const parsed = Number(value);
244
- if (Number.isFinite(parsed))
245
- return parsed;
246
- }
247
- return 0;
248
- }
249
- function asDate(value) {
250
- if (value instanceof Date)
251
- return value;
252
- if (typeof value === "string" || typeof value === "number") {
253
- const date = new Date(value);
254
- if (!Number.isNaN(date.getTime()))
255
- return date;
256
- }
257
- return null;
258
- }
259
- function toIsoString(value) {
260
- if (!value)
261
- return;
262
- return typeof value === "string" ? value : value.toISOString();
263
- }
264
- export {
265
- PosthogMeteringReader
266
- };
1
+ class R{reader;eventPrefix;constructor(e,r={}){this.reader=e,this.eventPrefix=r.eventPrefix??"metering"}async getUsageByMetric(e){let r=await this.queryHogQL({query:["select"," properties.recordId as recordId,"," properties.metricKey as metricKey,"," properties.subjectType as subjectType,"," distinct_id as subjectId,"," properties.quantity as quantity,"," properties.source as source,"," timestamp as timestamp","from events",`where ${b(this.eventPrefix,e)}`,"order by timestamp desc",`limit ${e.limit??200}`].join(`
2
+ `),values:l(e)});return j(r)}async getUsageSummary(e){let r=await this.queryHogQL({query:["select"," properties.summaryId as summaryId,"," properties.metricKey as metricKey,"," properties.subjectType as subjectType,"," distinct_id as subjectId,"," properties.periodType as periodType,"," properties.periodStart as periodStart,"," properties.periodEnd as periodEnd,"," properties.totalQuantity as totalQuantity,"," properties.recordCount as recordCount,"," timestamp as aggregatedAt","from events",`where ${I(this.eventPrefix,e)}`,"order by timestamp desc",`limit ${e.limit??200}`].join(`
3
+ `),values:T(e)});return S(r)}async getUsageTrend(e){let r=await this.queryHogQL({query:["select",` ${h(e.bucket)} as bucketStart,`," sum(properties.quantity) as totalQuantity,"," count() as recordCount","from events",`where ${b(this.eventPrefix,e)}`,"group by bucketStart","order by bucketStart asc"].join(`
4
+ `),values:l(e)});return U(r)}async queryHogQL(e){if(!this.reader.queryHogQL)throw Error("Analytics reader does not support HogQL queries.");return this.reader.queryHogQL(e)}}function b(e,r){let t=[`event = '${e}.usage_recorded'`,"properties.metricKey = {metricKey}"];if(r.subjectId)t.push("distinct_id = {subjectId}");if(r.dateRange?.from)t.push("timestamp >= {dateFrom}");if(r.dateRange?.to)t.push("timestamp < {dateTo}");return t.join(" and ")}function I(e,r){let t=[`event = '${e}.usage_aggregated'`,"properties.metricKey = {metricKey}"];if(r.subjectId)t.push("distinct_id = {subjectId}");if(r.periodType)t.push("properties.periodType = {periodType}");if(r.dateRange?.from)t.push("timestamp >= {dateFrom}");if(r.dateRange?.to)t.push("timestamp < {dateTo}");return t.join(" and ")}function l(e){return{metricKey:e.metricKey,subjectId:e.subjectId,dateFrom:d(e.dateRange?.from),dateTo:d(e.dateRange?.to)}}function T(e){return{metricKey:e.metricKey,subjectId:e.subjectId,periodType:e.periodType,dateFrom:d(e.dateRange?.from),dateTo:d(e.dateRange?.to)}}function h(e){switch(e){case"hour":return"toStartOfHour(timestamp)";case"week":return"toStartOfWeek(timestamp)";case"month":return"toStartOfMonth(timestamp)";case"day":default:return"toStartOfDay(timestamp)"}}function j(e){return p(e).flatMap((t)=>{let n=s(t.metricKey),i=s(t.subjectType),a=s(t.subjectId),o=c(t.timestamp);if(!n||!i||!a||!o)return[];return[{recordId:g(t.recordId),metricKey:n,subjectType:i,subjectId:a,quantity:u(t.quantity),source:g(t.source),timestamp:o}]})}function S(e){return p(e).flatMap((t)=>{let n=s(t.metricKey),i=s(t.subjectType),a=s(t.subjectId),o=s(t.periodType),m=c(t.periodStart),y=c(t.periodEnd),f=c(t.aggregatedAt);if(!n||!i||!a||!o||!m||!y||!f)return[];return[{summaryId:g(t.summaryId),metricKey:n,subjectType:i,subjectId:a,periodType:o,periodStart:m,periodEnd:y,totalQuantity:u(t.totalQuantity),recordCount:u(t.recordCount),aggregatedAt:f}]})}function U(e){return p(e).flatMap((t)=>{let n=s(t.bucketStart);if(!n)return[];return[{bucketStart:n,totalQuantity:u(t.totalQuantity),recordCount:u(t.recordCount)}]})}function p(e){if(!Array.isArray(e.results)||!Array.isArray(e.columns))return[];let r=e.columns;return e.results.flatMap((t)=>{if(!Array.isArray(t))return[];let n={};return r.forEach((i,a)=>{n[i]=t[a]}),[n]})}function s(e){if(typeof e==="string"&&e.trim())return e;if(typeof e==="number")return String(e);return null}function g(e){if(typeof e==="string")return e;if(typeof e==="number")return String(e);return}function u(e){if(typeof e==="number"&&Number.isFinite(e))return e;if(typeof e==="string"&&e.trim()){let r=Number(e);if(Number.isFinite(r))return r}return 0}function c(e){if(e instanceof Date)return e;if(typeof e==="string"||typeof e==="number"){let r=new Date(e);if(!Number.isNaN(r.getTime()))return r}return null}function d(e){if(!e)return;return typeof e==="string"?e:e.toISOString()}export{R as PosthogMeteringReader};
@@ -1,45 +1 @@
1
- // src/analytics/posthog-metering.ts
2
- class PosthogMeteringReporter {
3
- provider;
4
- eventPrefix;
5
- includeSource;
6
- constructor(provider, options = {}) {
7
- this.provider = provider;
8
- this.eventPrefix = options.eventPrefix ?? "metering";
9
- this.includeSource = options.includeSource ?? true;
10
- }
11
- async captureUsageRecorded(record) {
12
- await this.provider.capture({
13
- distinctId: record.subjectId,
14
- event: `${this.eventPrefix}.usage_recorded`,
15
- timestamp: record.timestamp,
16
- properties: {
17
- recordId: record.recordId ?? null,
18
- metricKey: record.metricKey,
19
- subjectType: record.subjectType,
20
- quantity: record.quantity,
21
- ...this.includeSource && record.source ? { source: record.source } : {}
22
- }
23
- });
24
- }
25
- async captureUsageAggregated(summary) {
26
- await this.provider.capture({
27
- distinctId: summary.subjectId,
28
- event: `${this.eventPrefix}.usage_aggregated`,
29
- timestamp: summary.aggregatedAt,
30
- properties: {
31
- summaryId: summary.summaryId ?? null,
32
- metricKey: summary.metricKey,
33
- subjectType: summary.subjectType,
34
- periodType: summary.periodType,
35
- periodStart: summary.periodStart.toISOString(),
36
- periodEnd: summary.periodEnd.toISOString(),
37
- totalQuantity: summary.totalQuantity,
38
- recordCount: summary.recordCount
39
- }
40
- });
41
- }
42
- }
43
- export {
44
- PosthogMeteringReporter
45
- };
1
+ class r{provider;eventPrefix;includeSource;constructor(e,t={}){this.provider=e,this.eventPrefix=t.eventPrefix??"metering",this.includeSource=t.includeSource??!0}async captureUsageRecorded(e){await this.provider.capture({distinctId:e.subjectId,event:`${this.eventPrefix}.usage_recorded`,timestamp:e.timestamp,properties:{recordId:e.recordId??null,metricKey:e.metricKey,subjectType:e.subjectType,quantity:e.quantity,...this.includeSource&&e.source?{source:e.source}:{}}})}async captureUsageAggregated(e){await this.provider.capture({distinctId:e.subjectId,event:`${this.eventPrefix}.usage_aggregated`,timestamp:e.aggregatedAt,properties:{summaryId:e.summaryId??null,metricKey:e.metricKey,subjectType:e.subjectType,periodType:e.periodType,periodStart:e.periodStart.toISOString(),periodEnd:e.periodEnd.toISOString(),totalQuantity:e.totalQuantity,recordCount:e.recordCount}})}}export{r as PosthogMeteringReporter};