@contractspec/lib.metering 1.56.1 → 1.57.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/aggregation/index.ts"],"sourcesContent":[],"mappings":";;AASA;AACA;AAEA;AASA;;AAMe,KAlBH,UAAA,GAkBG,QAAA,GAAA,OAAA,GAAA,QAAA,GAAA,SAAA,GAAA,QAAA;AACF,KAlBD,eAAA,GAkBC,OAAA,GAAA,KAAA,GAAA,KAAA,GAAA,KAAA,GAAA,KAAA,GAAA,MAAA;AAAI,UAhBA,WAAA,CAgBA;EAQA,EAAA,EAAA,MAAA;EAKA,SAAA,EAAA,MAAY;EAMZ,WAAA,EAAA,MAAA;EACF,SAAA,EAAA,MAAA;EAED,QAAA,EAAA,MAAA;EAAR,SAAA,EAhCO,IAgCP;;AAK4D,UAlCjD,YAAA,CAkCiD;EAKpC,EAAA,EAAA,MAAA;EAAL,SAAA,EAAA,MAAA;EAAmC,WAAA,EAAA,MAAA;EAAR,SAAA,EAAA,MAAA;EAKlB,UAAA,EAvCpB,UAuCoB;EAAR,WAAA,EAtCX,IAsCW;EAKD,SAAA,EA1CZ,IA0CY;EAAR,aAAA,EAAA,MAAA;EAAO,WAAA,EAAA,MAAA;EAGP,WAAA,CAAA,EAAA,MAAA;EAOA,WAAA,CAAA,EAAA,MAAe;EAElB,WAAA,CAAA,EAAA,MAAA;;AAIA,UAlDG,gBAAA,CAkDH;EAAI,GAAA,EAAA,MAAA;EAKD,eAAA,EArDE,eAqDe;;AAEnB,UApDE,YAAA,CAoDF;EACF;;;EAOI,sBAAgB,CAAA,OAAA,EAAA;IAYjB,SAAA,CAAA,EAAA,MAAc;IAAO,WAAA,EAlEpB,IAkEoB;IAAkB,SAAA,EAjExC,IAiEwC;IAAa,KAAA,CAAA,EAAA,MAAA;EAAI,CAAA,CAAA,EA/DlE,OA+DkE,CA/D1D,WA+D0D,EAAA,CAAA;EAkCxD;;;EAAkD,qBAAA,CAAA,SAAA,EAAA,MAAA,EAAA,EAAA,YAAA,EA5FP,IA4FO,CAAA,EA5FA,OA4FA,CAAA,IAAA,CAAA;EAAI;AA8BtE;AAyCA;EAIuB,aAAA,CAAA,OAAA,EAlKE,IAkKF,CAlKO,YAkKP,EAAA,IAAA,CAAA,CAAA,EAlK6B,OAkK7B,CAlKqC,YAkKrC,CAAA;EAQG;;;EAAyB,SAAA,CAAA,GAAA,EAAA,MAAA,CAAA,EArKzB,OAqKyB,CArKjB,gBAqKiB,GAAA,IAAA,CAAA;EAyKtC;;;EAeI,WAAA,EAAA,EAxVA,OAwVA,CAxVQ,gBAwVR,EAAA,CAAA;;AAGH,UAxVG,kBAAA,CAwVH;EAAR;EAgB8C,OAAA,EAtWzC,YAsWyC;EAKlC;EAAL,SAAA,CAAA,EAAA,MAAA;;AACR,UAvWY,eAAA,CAuWZ;EAgCmC;EAAR,UAAA,EArYlB,UAqYkB;EAID;EAAR,WAAA,EAvYR,IAuYQ;EAIL;EAhF2B,SAAA,CAAA,EAzT/B,IAyT+B;EAAY;;;UApTxC,iBAAA;cACH;eACC;aACF;;;;UAIH;;UAGO,gBAAA;;;;;;;;;iBAYD,cAAA,OAAqB,kBAAkB,aAAa;;;;iBAkCpD,YAAA,OAAmB,kBAAkB,aAAa;;;;iBA8BlD,eAAA,OAAsB,kBAAkB;;;;;;cAyC3C,eAAA;;;uBAIU;;;;oBAQG,kBAAkB,QAAQ;;;;;;;;;;;;;;;;;cAyKvC,oBAAA,YAAgC;;;;oBAKzB;oBAIA;;;iBAMH;eACF;;MAET,QAAQ;8CAgBsC;yBAKvC,KAAK,sBACb,QAAQ;0BAgCmB,QAAQ;iBAIjB,QAAQ;kBAIb"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/aggregation/index.ts"],"mappings":";;AASA;;;;;KAAY,UAAA;AAAA,KACA,eAAA;AAAA,UAEK,WAAA;EACf,EAAA;EACA,SAAA;EACA,WAAA;EACA,SAAA;EACA,QAAA;EACA,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,YAAA;EACf,EAAA;EACA,SAAA;EACA,WAAA;EACA,SAAA;EACA,UAAA,EAAY,UAAA;EACZ,WAAA,EAAa,IAAA;EACb,SAAA,EAAW,IAAA;EACX,aAAA;EACA,WAAA;EACA,WAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,UAGe,gBAAA;EACf,GAAA;EACA,eAAA,EAAiB,eAAA;AAAA;AAAA,UAGF,YAAA;EAhBf;;;EAoBA,sBAAA,CAAuB,OAAA;IACrB,SAAA;IACA,WAAA,EAAa,IAAA;IACb,SAAA,EAAW,IAAA;IACX,KAAA;EAAA,IACE,OAAA,CAAQ,WAAA;EAnBZ;;;EAwBA,qBAAA,CAAsB,SAAA,YAAqB,YAAA,EAAc,IAAA,GAAO,OAAA;EAtBrD;AAGb;;EAwBE,aAAA,CAAc,OAAA,EAAS,IAAA,CAAK,YAAA,UAAsB,OAAA,CAAQ,YAAA;EAtB1B;;;EA2BhC,SAAA,CAAU,GAAA,WAAc,OAAA,CAAQ,gBAAA;EA3BA;;AAGlC;EA6BE,WAAA,IAAe,OAAA,CAAQ,gBAAA;AAAA;AAAA,UAGR,kBAAA;EAzBF;EA2Bb,OAAA,EAAS,YAAA;EAzBL;EA2BJ,SAAA;AAAA;AAAA,UAGe,eAAA;EApBQ;EAsBvB,UAAA,EAAY,UAAA;EAtBsC;EAwBlD,WAAA,EAAa,IAAA;EAnBW;EAqBxB,SAAA,GAAY,IAAA;EAhBG;EAkBf,SAAA;AAAA;AAAA,UAGe,iBAAA;EACf,UAAA,EAAY,UAAA;EACZ,WAAA,EAAa,IAAA;EACb,SAAA,EAAW,IAAA;EACX,gBAAA;EACA,gBAAA;EACA,gBAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;AAAA,UAGO,gBAAA;EACf,SAAA;EACA,WAAA;EACA,SAAA;EACA,KAAA;AAAA;;;;iBAQc,cAAA,CAAe,IAAA,EAAM,IAAA,EAAM,UAAA,EAAY,UAAA,GAAa,IAAA;;;;iBAkCpD,YAAA,CAAa,IAAA,EAAM,IAAA,EAAM,UAAA,EAAY,UAAA,GAAa,IAAA;;;;iBA8BlD,eAAA,CAAgB,IAAA,EAAM,IAAA,EAAM,UAAA,EAAY,UAAA;;;;AAxGxD;;cAiJa,eAAA;EAAA,QACH,OAAA;EAAA,QACA,SAAA;cAEI,OAAA,EAAS,kBAAA;EAjJrB;;;EAyJM,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,OAAA,CAAQ,iBAAA;EAtJpB;;;EAAA,QA6MtB,YAAA;EAvMI;;;EAAA,QA4NE,cAAA;EAhOF;;;EAAA,QA8QJ,oBAAA;AAAA;;;;cA+CG,oBAAA,YAAgC,YAAA;EAAA,QACnC,OAAA;EAAA,QACA,SAAA;EAAA,QACA,OAAA;EAER,SAAA,CAAU,MAAA,EAAQ,WAAA;EAIlB,SAAA,CAAU,MAAA,EAAQ,gBAAA;EAIZ,sBAAA,CAAuB,OAAA;IAC3B,SAAA;IACA,WAAA,EAAa,IAAA;IACb,SAAA,EAAW,IAAA;IACX,KAAA;EAAA,IACE,OAAA,CAAQ,WAAA;EAgBN,qBAAA,CAAsB,SAAA,aAAsB,OAAA;EAI5C,aAAA,CACJ,OAAA,EAAS,IAAA,CAAK,YAAA,UACb,OAAA,CAAQ,YAAA;EAgCL,SAAA,CAAU,GAAA,WAAc,OAAA,CAAQ,gBAAA;EAIhC,WAAA,CAAA,GAAe,OAAA,CAAQ,gBAAA;EAI7B,YAAA,CAAA,GAAgB,YAAA;EAIhB,KAAA,CAAA;AAAA"}
@@ -111,9 +111,9 @@ var UsageAggregator = class {
111
111
  for (const [groupKey, groupRecords] of groups.entries()) try {
112
112
  await this.aggregateGroup(groupKey, groupRecords, periodType, result);
113
113
  } catch (error) {
114
- const [metricKey$1, subjectType, subjectId] = groupKey.split("::");
114
+ const [metricKey, subjectType, subjectId] = groupKey.split("::");
115
115
  result.errors.push({
116
- metricKey: metricKey$1 ?? "unknown",
116
+ metricKey: metricKey ?? "unknown",
117
117
  subjectType: subjectType ?? "unknown",
118
118
  subjectId: subjectId ?? "unknown",
119
119
  error: error instanceof Error ? error.message : String(error)
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["metricKey"],"sources":["../../src/aggregation/index.ts"],"sourcesContent":["/**\n * Usage aggregation engine.\n *\n * Provides periodic aggregation of usage records into summaries\n * for efficient billing and reporting queries.\n */\n\n// ============ Types ============\n\nexport type PeriodType = 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';\nexport type AggregationType = 'COUNT' | 'SUM' | 'AVG' | 'MAX' | 'MIN' | 'LAST';\n\nexport interface UsageRecord {\n id: string;\n metricKey: string;\n subjectType: string;\n subjectId: string;\n quantity: number;\n timestamp: Date;\n}\n\nexport interface UsageSummary {\n id: string;\n metricKey: string;\n subjectType: string;\n subjectId: string;\n periodType: PeriodType;\n periodStart: Date;\n periodEnd: Date;\n totalQuantity: number;\n recordCount: number;\n minQuantity?: number;\n maxQuantity?: number;\n avgQuantity?: number;\n}\n\nexport interface MetricDefinition {\n key: string;\n aggregationType: AggregationType;\n}\n\nexport interface UsageStorage {\n /**\n * Get unaggregated records for a period.\n */\n getUnaggregatedRecords(options: {\n metricKey?: string;\n periodStart: Date;\n periodEnd: Date;\n limit?: number;\n }): Promise<UsageRecord[]>;\n\n /**\n * Mark records as aggregated.\n */\n markRecordsAggregated(recordIds: string[], aggregatedAt: Date): Promise<void>;\n\n /**\n * Get or create a summary record.\n */\n upsertSummary(summary: Omit<UsageSummary, 'id'>): Promise<UsageSummary>;\n\n /**\n * Get metric definition.\n */\n getMetric(key: string): Promise<MetricDefinition | null>;\n\n /**\n * List all active metrics.\n */\n listMetrics(): Promise<MetricDefinition[]>;\n}\n\nexport interface AggregationOptions {\n /** Storage implementation */\n storage: UsageStorage;\n /** Batch size for processing records */\n batchSize?: number;\n}\n\nexport interface AggregateParams {\n /** Period type to aggregate */\n periodType: PeriodType;\n /** Period start time */\n periodStart: Date;\n /** Period end time (optional, defaults to period boundary) */\n periodEnd?: Date;\n /** Specific metric to aggregate (optional, aggregates all if not specified) */\n metricKey?: string;\n}\n\nexport interface AggregationResult {\n periodType: PeriodType;\n periodStart: Date;\n periodEnd: Date;\n recordsProcessed: number;\n summariesCreated: number;\n summariesUpdated: number;\n errors: AggregationError[];\n}\n\nexport interface AggregationError {\n metricKey: string;\n subjectType: string;\n subjectId: string;\n error: string;\n}\n\n// ============ Period Helpers ============\n\n/**\n * Get the start of a period for a given date.\n */\nexport function getPeriodStart(date: Date, periodType: PeriodType): Date {\n const d = new Date(date);\n\n switch (periodType) {\n case 'HOURLY':\n d.setMinutes(0, 0, 0);\n return d;\n\n case 'DAILY':\n d.setHours(0, 0, 0, 0);\n return d;\n\n case 'WEEKLY': {\n d.setHours(0, 0, 0, 0);\n const day = d.getDay();\n d.setDate(d.getDate() - day);\n return d;\n }\n\n case 'MONTHLY':\n d.setHours(0, 0, 0, 0);\n d.setDate(1);\n return d;\n\n case 'YEARLY':\n d.setHours(0, 0, 0, 0);\n d.setMonth(0, 1);\n return d;\n }\n}\n\n/**\n * Get the end of a period for a given date.\n */\nexport function getPeriodEnd(date: Date, periodType: PeriodType): Date {\n const start = getPeriodStart(date, periodType);\n\n switch (periodType) {\n case 'HOURLY':\n return new Date(start.getTime() + 60 * 60 * 1000);\n\n case 'DAILY':\n return new Date(start.getTime() + 24 * 60 * 60 * 1000);\n\n case 'WEEKLY':\n return new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);\n\n case 'MONTHLY': {\n const end = new Date(start);\n end.setMonth(end.getMonth() + 1);\n return end;\n }\n\n case 'YEARLY': {\n const end = new Date(start);\n end.setFullYear(end.getFullYear() + 1);\n return end;\n }\n }\n}\n\n/**\n * Format a period key for grouping.\n */\nexport function formatPeriodKey(date: Date, periodType: PeriodType): string {\n const start = getPeriodStart(date, periodType);\n const year = start.getFullYear();\n const month = String(start.getMonth() + 1).padStart(2, '0');\n const day = String(start.getDate()).padStart(2, '0');\n const hour = String(start.getHours()).padStart(2, '0');\n\n switch (periodType) {\n case 'HOURLY':\n return `${year}-${month}-${day}T${hour}`;\n case 'DAILY':\n return `${year}-${month}-${day}`;\n case 'WEEKLY':\n return `${year}-W${getWeekNumber(start)}`;\n case 'MONTHLY':\n return `${year}-${month}`;\n case 'YEARLY':\n return `${year}`;\n }\n}\n\nfunction getWeekNumber(date: Date): string {\n const d = new Date(\n Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())\n );\n const dayNum = d.getUTCDay() || 7;\n d.setUTCDate(d.getUTCDate() + 4 - dayNum);\n const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n const weekNum = Math.ceil(\n ((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7\n );\n return String(weekNum).padStart(2, '0');\n}\n\n// ============ Aggregator ============\n\n/**\n * Usage aggregator.\n *\n * Aggregates usage records into summaries based on period type.\n */\nexport class UsageAggregator {\n private storage: UsageStorage;\n private batchSize: number;\n\n constructor(options: AggregationOptions) {\n this.storage = options.storage;\n this.batchSize = options.batchSize || 1000;\n }\n\n /**\n * Aggregate usage records for a period.\n */\n async aggregate(params: AggregateParams): Promise<AggregationResult> {\n const { periodType, periodStart, metricKey } = params;\n const periodEnd = params.periodEnd || getPeriodEnd(periodStart, periodType);\n\n const result: AggregationResult = {\n periodType,\n periodStart,\n periodEnd,\n recordsProcessed: 0,\n summariesCreated: 0,\n summariesUpdated: 0,\n errors: [],\n };\n\n // Get records to aggregate\n const records = await this.storage.getUnaggregatedRecords({\n metricKey,\n periodStart,\n periodEnd,\n limit: this.batchSize,\n });\n\n if (records.length === 0) {\n return result;\n }\n\n // Group records by metric, subject, and period\n const groups = this.groupRecords(records, periodType);\n\n // Process each group\n for (const [groupKey, groupRecords] of groups.entries()) {\n try {\n await this.aggregateGroup(groupKey, groupRecords, periodType, result);\n } catch (error) {\n const [metricKey, subjectType, subjectId] = groupKey.split('::');\n result.errors.push({\n metricKey: metricKey ?? 'unknown',\n subjectType: subjectType ?? 'unknown',\n subjectId: subjectId ?? 'unknown',\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // Mark records as aggregated\n const recordIds = records.map((r) => r.id);\n await this.storage.markRecordsAggregated(recordIds, new Date());\n result.recordsProcessed = records.length;\n\n return result;\n }\n\n /**\n * Group records by metric, subject, and period.\n */\n private groupRecords(\n records: UsageRecord[],\n periodType: PeriodType\n ): Map<string, UsageRecord[]> {\n const groups = new Map<string, UsageRecord[]>();\n\n for (const record of records) {\n const periodKey = formatPeriodKey(record.timestamp, periodType);\n const groupKey = `${record.metricKey}::${record.subjectType}::${record.subjectId}::${periodKey}`;\n\n const existing = groups.get(groupKey) || [];\n existing.push(record);\n groups.set(groupKey, existing);\n }\n\n return groups;\n }\n\n /**\n * Aggregate a group of records into a summary.\n */\n private async aggregateGroup(\n groupKey: string,\n records: UsageRecord[],\n periodType: PeriodType,\n result: AggregationResult\n ): Promise<void> {\n const [metricKey, subjectType, subjectId] = groupKey.split('::');\n\n if (!metricKey || !subjectType || !subjectId || records.length === 0) {\n return;\n }\n\n const firstRecord = records[0];\n if (!firstRecord) return;\n const periodStart = getPeriodStart(firstRecord.timestamp, periodType);\n const periodEnd = getPeriodEnd(firstRecord.timestamp, periodType);\n\n // Get metric definition for aggregation type\n const metric = await this.storage.getMetric(metricKey);\n const aggregationType = metric?.aggregationType || 'SUM';\n\n // Calculate aggregated values\n const quantities = records.map((r) => r.quantity);\n const aggregated = this.calculateAggregation(quantities, aggregationType);\n\n // Create or update summary\n await this.storage.upsertSummary({\n metricKey,\n subjectType,\n subjectId,\n periodType,\n periodStart,\n periodEnd,\n totalQuantity: aggregated.total,\n recordCount: records.length,\n minQuantity: aggregated.min,\n maxQuantity: aggregated.max,\n avgQuantity: aggregated.avg,\n });\n\n result.summariesCreated++;\n }\n\n /**\n * Calculate aggregation values.\n */\n private calculateAggregation(\n quantities: number[],\n aggregationType: AggregationType\n ): { total: number; min: number; max: number; avg: number } {\n if (quantities.length === 0) {\n return { total: 0, min: 0, max: 0, avg: 0 };\n }\n\n const min = Math.min(...quantities);\n const max = Math.max(...quantities);\n const sum = quantities.reduce((a, b) => a + b, 0);\n const avg = sum / quantities.length;\n const count = quantities.length;\n\n let total: number;\n switch (aggregationType) {\n case 'COUNT':\n total = count;\n break;\n case 'SUM':\n total = sum;\n break;\n case 'AVG':\n total = avg;\n break;\n case 'MAX':\n total = max;\n break;\n case 'MIN':\n total = min;\n break;\n case 'LAST':\n total = quantities[quantities.length - 1] ?? 0;\n break;\n default:\n total = sum;\n }\n\n return { total, min, max, avg };\n }\n}\n\n// ============ In-Memory Storage ============\n\n/**\n * In-memory usage storage for testing.\n */\nexport class InMemoryUsageStorage implements UsageStorage {\n private records: UsageRecord[] = [];\n private summaries = new Map<string, UsageSummary>();\n private metrics = new Map<string, MetricDefinition>();\n\n addRecord(record: UsageRecord): void {\n this.records.push(record);\n }\n\n addMetric(metric: MetricDefinition): void {\n this.metrics.set(metric.key, metric);\n }\n\n async getUnaggregatedRecords(options: {\n metricKey?: string;\n periodStart: Date;\n periodEnd: Date;\n limit?: number;\n }): Promise<UsageRecord[]> {\n let records = this.records.filter((r) => {\n const inPeriod =\n r.timestamp >= options.periodStart && r.timestamp < options.periodEnd;\n const matchesMetric =\n !options.metricKey || r.metricKey === options.metricKey;\n return inPeriod && matchesMetric;\n });\n\n if (options.limit) {\n records = records.slice(0, options.limit);\n }\n\n return records;\n }\n\n async markRecordsAggregated(recordIds: string[]): Promise<void> {\n this.records = this.records.filter((r) => !recordIds.includes(r.id));\n }\n\n async upsertSummary(\n summary: Omit<UsageSummary, 'id'>\n ): Promise<UsageSummary> {\n const key = `${summary.metricKey}::${summary.subjectType}::${summary.subjectId}::${summary.periodType}::${summary.periodStart.toISOString()}`;\n\n const existing = this.summaries.get(key);\n if (existing) {\n // Update existing summary\n existing.totalQuantity += summary.totalQuantity;\n existing.recordCount += summary.recordCount;\n if (summary.minQuantity !== undefined) {\n existing.minQuantity = Math.min(\n existing.minQuantity ?? Infinity,\n summary.minQuantity\n );\n }\n if (summary.maxQuantity !== undefined) {\n existing.maxQuantity = Math.max(\n existing.maxQuantity ?? -Infinity,\n summary.maxQuantity\n );\n }\n return existing;\n }\n\n // Create new summary\n const newSummary: UsageSummary = {\n id: `summary-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n ...summary,\n };\n this.summaries.set(key, newSummary);\n return newSummary;\n }\n\n async getMetric(key: string): Promise<MetricDefinition | null> {\n return this.metrics.get(key) || null;\n }\n\n async listMetrics(): Promise<MetricDefinition[]> {\n return Array.from(this.metrics.values());\n }\n\n getSummaries(): UsageSummary[] {\n return Array.from(this.summaries.values());\n }\n\n clear(): void {\n this.records = [];\n this.summaries.clear();\n this.metrics.clear();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/aggregation/index.ts"],"sourcesContent":["/**\n * Usage aggregation engine.\n *\n * Provides periodic aggregation of usage records into summaries\n * for efficient billing and reporting queries.\n */\n\n// ============ Types ============\n\nexport type PeriodType = 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';\nexport type AggregationType = 'COUNT' | 'SUM' | 'AVG' | 'MAX' | 'MIN' | 'LAST';\n\nexport interface UsageRecord {\n id: string;\n metricKey: string;\n subjectType: string;\n subjectId: string;\n quantity: number;\n timestamp: Date;\n}\n\nexport interface UsageSummary {\n id: string;\n metricKey: string;\n subjectType: string;\n subjectId: string;\n periodType: PeriodType;\n periodStart: Date;\n periodEnd: Date;\n totalQuantity: number;\n recordCount: number;\n minQuantity?: number;\n maxQuantity?: number;\n avgQuantity?: number;\n}\n\nexport interface MetricDefinition {\n key: string;\n aggregationType: AggregationType;\n}\n\nexport interface UsageStorage {\n /**\n * Get unaggregated records for a period.\n */\n getUnaggregatedRecords(options: {\n metricKey?: string;\n periodStart: Date;\n periodEnd: Date;\n limit?: number;\n }): Promise<UsageRecord[]>;\n\n /**\n * Mark records as aggregated.\n */\n markRecordsAggregated(recordIds: string[], aggregatedAt: Date): Promise<void>;\n\n /**\n * Get or create a summary record.\n */\n upsertSummary(summary: Omit<UsageSummary, 'id'>): Promise<UsageSummary>;\n\n /**\n * Get metric definition.\n */\n getMetric(key: string): Promise<MetricDefinition | null>;\n\n /**\n * List all active metrics.\n */\n listMetrics(): Promise<MetricDefinition[]>;\n}\n\nexport interface AggregationOptions {\n /** Storage implementation */\n storage: UsageStorage;\n /** Batch size for processing records */\n batchSize?: number;\n}\n\nexport interface AggregateParams {\n /** Period type to aggregate */\n periodType: PeriodType;\n /** Period start time */\n periodStart: Date;\n /** Period end time (optional, defaults to period boundary) */\n periodEnd?: Date;\n /** Specific metric to aggregate (optional, aggregates all if not specified) */\n metricKey?: string;\n}\n\nexport interface AggregationResult {\n periodType: PeriodType;\n periodStart: Date;\n periodEnd: Date;\n recordsProcessed: number;\n summariesCreated: number;\n summariesUpdated: number;\n errors: AggregationError[];\n}\n\nexport interface AggregationError {\n metricKey: string;\n subjectType: string;\n subjectId: string;\n error: string;\n}\n\n// ============ Period Helpers ============\n\n/**\n * Get the start of a period for a given date.\n */\nexport function getPeriodStart(date: Date, periodType: PeriodType): Date {\n const d = new Date(date);\n\n switch (periodType) {\n case 'HOURLY':\n d.setMinutes(0, 0, 0);\n return d;\n\n case 'DAILY':\n d.setHours(0, 0, 0, 0);\n return d;\n\n case 'WEEKLY': {\n d.setHours(0, 0, 0, 0);\n const day = d.getDay();\n d.setDate(d.getDate() - day);\n return d;\n }\n\n case 'MONTHLY':\n d.setHours(0, 0, 0, 0);\n d.setDate(1);\n return d;\n\n case 'YEARLY':\n d.setHours(0, 0, 0, 0);\n d.setMonth(0, 1);\n return d;\n }\n}\n\n/**\n * Get the end of a period for a given date.\n */\nexport function getPeriodEnd(date: Date, periodType: PeriodType): Date {\n const start = getPeriodStart(date, periodType);\n\n switch (periodType) {\n case 'HOURLY':\n return new Date(start.getTime() + 60 * 60 * 1000);\n\n case 'DAILY':\n return new Date(start.getTime() + 24 * 60 * 60 * 1000);\n\n case 'WEEKLY':\n return new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);\n\n case 'MONTHLY': {\n const end = new Date(start);\n end.setMonth(end.getMonth() + 1);\n return end;\n }\n\n case 'YEARLY': {\n const end = new Date(start);\n end.setFullYear(end.getFullYear() + 1);\n return end;\n }\n }\n}\n\n/**\n * Format a period key for grouping.\n */\nexport function formatPeriodKey(date: Date, periodType: PeriodType): string {\n const start = getPeriodStart(date, periodType);\n const year = start.getFullYear();\n const month = String(start.getMonth() + 1).padStart(2, '0');\n const day = String(start.getDate()).padStart(2, '0');\n const hour = String(start.getHours()).padStart(2, '0');\n\n switch (periodType) {\n case 'HOURLY':\n return `${year}-${month}-${day}T${hour}`;\n case 'DAILY':\n return `${year}-${month}-${day}`;\n case 'WEEKLY':\n return `${year}-W${getWeekNumber(start)}`;\n case 'MONTHLY':\n return `${year}-${month}`;\n case 'YEARLY':\n return `${year}`;\n }\n}\n\nfunction getWeekNumber(date: Date): string {\n const d = new Date(\n Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())\n );\n const dayNum = d.getUTCDay() || 7;\n d.setUTCDate(d.getUTCDate() + 4 - dayNum);\n const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n const weekNum = Math.ceil(\n ((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7\n );\n return String(weekNum).padStart(2, '0');\n}\n\n// ============ Aggregator ============\n\n/**\n * Usage aggregator.\n *\n * Aggregates usage records into summaries based on period type.\n */\nexport class UsageAggregator {\n private storage: UsageStorage;\n private batchSize: number;\n\n constructor(options: AggregationOptions) {\n this.storage = options.storage;\n this.batchSize = options.batchSize || 1000;\n }\n\n /**\n * Aggregate usage records for a period.\n */\n async aggregate(params: AggregateParams): Promise<AggregationResult> {\n const { periodType, periodStart, metricKey } = params;\n const periodEnd = params.periodEnd || getPeriodEnd(periodStart, periodType);\n\n const result: AggregationResult = {\n periodType,\n periodStart,\n periodEnd,\n recordsProcessed: 0,\n summariesCreated: 0,\n summariesUpdated: 0,\n errors: [],\n };\n\n // Get records to aggregate\n const records = await this.storage.getUnaggregatedRecords({\n metricKey,\n periodStart,\n periodEnd,\n limit: this.batchSize,\n });\n\n if (records.length === 0) {\n return result;\n }\n\n // Group records by metric, subject, and period\n const groups = this.groupRecords(records, periodType);\n\n // Process each group\n for (const [groupKey, groupRecords] of groups.entries()) {\n try {\n await this.aggregateGroup(groupKey, groupRecords, periodType, result);\n } catch (error) {\n const [metricKey, subjectType, subjectId] = groupKey.split('::');\n result.errors.push({\n metricKey: metricKey ?? 'unknown',\n subjectType: subjectType ?? 'unknown',\n subjectId: subjectId ?? 'unknown',\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // Mark records as aggregated\n const recordIds = records.map((r) => r.id);\n await this.storage.markRecordsAggregated(recordIds, new Date());\n result.recordsProcessed = records.length;\n\n return result;\n }\n\n /**\n * Group records by metric, subject, and period.\n */\n private groupRecords(\n records: UsageRecord[],\n periodType: PeriodType\n ): Map<string, UsageRecord[]> {\n const groups = new Map<string, UsageRecord[]>();\n\n for (const record of records) {\n const periodKey = formatPeriodKey(record.timestamp, periodType);\n const groupKey = `${record.metricKey}::${record.subjectType}::${record.subjectId}::${periodKey}`;\n\n const existing = groups.get(groupKey) || [];\n existing.push(record);\n groups.set(groupKey, existing);\n }\n\n return groups;\n }\n\n /**\n * Aggregate a group of records into a summary.\n */\n private async aggregateGroup(\n groupKey: string,\n records: UsageRecord[],\n periodType: PeriodType,\n result: AggregationResult\n ): Promise<void> {\n const [metricKey, subjectType, subjectId] = groupKey.split('::');\n\n if (!metricKey || !subjectType || !subjectId || records.length === 0) {\n return;\n }\n\n const firstRecord = records[0];\n if (!firstRecord) return;\n const periodStart = getPeriodStart(firstRecord.timestamp, periodType);\n const periodEnd = getPeriodEnd(firstRecord.timestamp, periodType);\n\n // Get metric definition for aggregation type\n const metric = await this.storage.getMetric(metricKey);\n const aggregationType = metric?.aggregationType || 'SUM';\n\n // Calculate aggregated values\n const quantities = records.map((r) => r.quantity);\n const aggregated = this.calculateAggregation(quantities, aggregationType);\n\n // Create or update summary\n await this.storage.upsertSummary({\n metricKey,\n subjectType,\n subjectId,\n periodType,\n periodStart,\n periodEnd,\n totalQuantity: aggregated.total,\n recordCount: records.length,\n minQuantity: aggregated.min,\n maxQuantity: aggregated.max,\n avgQuantity: aggregated.avg,\n });\n\n result.summariesCreated++;\n }\n\n /**\n * Calculate aggregation values.\n */\n private calculateAggregation(\n quantities: number[],\n aggregationType: AggregationType\n ): { total: number; min: number; max: number; avg: number } {\n if (quantities.length === 0) {\n return { total: 0, min: 0, max: 0, avg: 0 };\n }\n\n const min = Math.min(...quantities);\n const max = Math.max(...quantities);\n const sum = quantities.reduce((a, b) => a + b, 0);\n const avg = sum / quantities.length;\n const count = quantities.length;\n\n let total: number;\n switch (aggregationType) {\n case 'COUNT':\n total = count;\n break;\n case 'SUM':\n total = sum;\n break;\n case 'AVG':\n total = avg;\n break;\n case 'MAX':\n total = max;\n break;\n case 'MIN':\n total = min;\n break;\n case 'LAST':\n total = quantities[quantities.length - 1] ?? 0;\n break;\n default:\n total = sum;\n }\n\n return { total, min, max, avg };\n }\n}\n\n// ============ In-Memory Storage ============\n\n/**\n * In-memory usage storage for testing.\n */\nexport class InMemoryUsageStorage implements UsageStorage {\n private records: UsageRecord[] = [];\n private summaries = new Map<string, UsageSummary>();\n private metrics = new Map<string, MetricDefinition>();\n\n addRecord(record: UsageRecord): void {\n this.records.push(record);\n }\n\n addMetric(metric: MetricDefinition): void {\n this.metrics.set(metric.key, metric);\n }\n\n async getUnaggregatedRecords(options: {\n metricKey?: string;\n periodStart: Date;\n periodEnd: Date;\n limit?: number;\n }): Promise<UsageRecord[]> {\n let records = this.records.filter((r) => {\n const inPeriod =\n r.timestamp >= options.periodStart && r.timestamp < options.periodEnd;\n const matchesMetric =\n !options.metricKey || r.metricKey === options.metricKey;\n return inPeriod && matchesMetric;\n });\n\n if (options.limit) {\n records = records.slice(0, options.limit);\n }\n\n return records;\n }\n\n async markRecordsAggregated(recordIds: string[]): Promise<void> {\n this.records = this.records.filter((r) => !recordIds.includes(r.id));\n }\n\n async upsertSummary(\n summary: Omit<UsageSummary, 'id'>\n ): Promise<UsageSummary> {\n const key = `${summary.metricKey}::${summary.subjectType}::${summary.subjectId}::${summary.periodType}::${summary.periodStart.toISOString()}`;\n\n const existing = this.summaries.get(key);\n if (existing) {\n // Update existing summary\n existing.totalQuantity += summary.totalQuantity;\n existing.recordCount += summary.recordCount;\n if (summary.minQuantity !== undefined) {\n existing.minQuantity = Math.min(\n existing.minQuantity ?? Infinity,\n summary.minQuantity\n );\n }\n if (summary.maxQuantity !== undefined) {\n existing.maxQuantity = Math.max(\n existing.maxQuantity ?? -Infinity,\n summary.maxQuantity\n );\n }\n return existing;\n }\n\n // Create new summary\n const newSummary: UsageSummary = {\n id: `summary-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n ...summary,\n };\n this.summaries.set(key, newSummary);\n return newSummary;\n }\n\n async getMetric(key: string): Promise<MetricDefinition | null> {\n return this.metrics.get(key) || null;\n }\n\n async listMetrics(): Promise<MetricDefinition[]> {\n return Array.from(this.metrics.values());\n }\n\n getSummaries(): UsageSummary[] {\n return Array.from(this.summaries.values());\n }\n\n clear(): void {\n this.records = [];\n this.summaries.clear();\n this.metrics.clear();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { MeteringUsageAggregate, MeteringUsageRecord } from "./posthog-metering.js";
2
+ import { AnalyticsReader, DateRangeInput } from "@contractspec/lib.contracts/integrations/providers/analytics";
3
+
4
+ //#region src/analytics/posthog-metering-reader.d.ts
5
+ interface UsageByMetricInput {
6
+ metricKey: string;
7
+ dateRange?: DateRangeInput;
8
+ subjectId?: string;
9
+ limit?: number;
10
+ }
11
+ interface UsageSummaryInput {
12
+ metricKey: string;
13
+ subjectId?: string;
14
+ periodType?: string;
15
+ dateRange?: DateRangeInput;
16
+ limit?: number;
17
+ }
18
+ type UsageTrendBucket = 'hour' | 'day' | 'week' | 'month';
19
+ interface UsageTrendInput {
20
+ metricKey: string;
21
+ bucket: UsageTrendBucket;
22
+ dateRange?: DateRangeInput;
23
+ }
24
+ interface MeteringUsageTrendPoint {
25
+ bucketStart: string;
26
+ totalQuantity: number;
27
+ recordCount: number;
28
+ }
29
+ interface PosthogMeteringReaderOptions {
30
+ eventPrefix?: string;
31
+ }
32
+ declare class PosthogMeteringReader {
33
+ private readonly reader;
34
+ private readonly eventPrefix;
35
+ constructor(reader: AnalyticsReader, options?: PosthogMeteringReaderOptions);
36
+ getUsageByMetric(input: UsageByMetricInput): Promise<MeteringUsageRecord[]>;
37
+ getUsageSummary(input: UsageSummaryInput): Promise<MeteringUsageAggregate[]>;
38
+ getUsageTrend(input: UsageTrendInput): Promise<MeteringUsageTrendPoint[]>;
39
+ private queryHogQL;
40
+ }
41
+ //#endregion
42
+ export { MeteringUsageTrendPoint, PosthogMeteringReader, PosthogMeteringReaderOptions, UsageByMetricInput, UsageSummaryInput, UsageTrendBucket, UsageTrendInput };
43
+ //# sourceMappingURL=posthog-metering-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posthog-metering-reader.d.ts","names":[],"sources":["../../src/analytics/posthog-metering-reader.ts"],"mappings":";;;;UAUiB,kBAAA;EACf,SAAA;EACA,SAAA,GAAY,cAAA;EACZ,SAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EACf,SAAA;EACA,SAAA;EACA,UAAA;EACA,SAAA,GAAY,cAAA;EACZ,KAAA;AAAA;AAAA,KAGU,gBAAA;AAAA,UAEK,eAAA;EACf,SAAA;EACA,MAAA,EAAQ,gBAAA;EACR,SAAA,GAAY,cAAA;AAAA;AAAA,UAGG,uBAAA;EACf,WAAA;EACA,aAAA;EACA,WAAA;AAAA;AAAA,UAGe,4BAAA;EACf,WAAA;AAAA;AAAA,cAGW,qBAAA;EAAA,iBACM,MAAA;EAAA,iBACA,WAAA;cAGf,MAAA,EAAQ,eAAA,EACR,OAAA,GAAS,4BAAA;EAML,gBAAA,CACJ,KAAA,EAAO,kBAAA,GACN,OAAA,CAAQ,mBAAA;EAsBL,eAAA,CACJ,KAAA,EAAO,iBAAA,GACN,OAAA,CAAQ,sBAAA;EAyBL,aAAA,CACJ,KAAA,EAAO,eAAA,GACN,OAAA,CAAQ,uBAAA;EAAA,QAkBG,UAAA;AAAA"}
@@ -0,0 +1,207 @@
1
+ //#region src/analytics/posthog-metering-reader.ts
2
+ var PosthogMeteringReader = class {
3
+ reader;
4
+ eventPrefix;
5
+ constructor(reader, options = {}) {
6
+ this.reader = reader;
7
+ this.eventPrefix = options.eventPrefix ?? "metering";
8
+ }
9
+ async getUsageByMetric(input) {
10
+ return mapUsageRecords(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("\n"),
25
+ values: buildUsageValues(input)
26
+ }));
27
+ }
28
+ async getUsageSummary(input) {
29
+ return mapUsageSummaries(await this.queryHogQL({
30
+ query: [
31
+ "select",
32
+ " properties.summaryId as summaryId,",
33
+ " properties.metricKey as metricKey,",
34
+ " properties.subjectType as subjectType,",
35
+ " distinct_id as subjectId,",
36
+ " properties.periodType as periodType,",
37
+ " properties.periodStart as periodStart,",
38
+ " properties.periodEnd as periodEnd,",
39
+ " properties.totalQuantity as totalQuantity,",
40
+ " properties.recordCount as recordCount,",
41
+ " timestamp as aggregatedAt",
42
+ "from events",
43
+ `where ${buildSummaryWhereClause(this.eventPrefix, input)}`,
44
+ "order by timestamp desc",
45
+ `limit ${input.limit ?? 200}`
46
+ ].join("\n"),
47
+ values: buildSummaryValues(input)
48
+ }));
49
+ }
50
+ async getUsageTrend(input) {
51
+ return mapUsageTrend(await this.queryHogQL({
52
+ query: [
53
+ "select",
54
+ ` ${bucketExpression(input.bucket)} as bucketStart,`,
55
+ " sum(properties.quantity) as totalQuantity,",
56
+ " count() as recordCount",
57
+ "from events",
58
+ `where ${buildUsageWhereClause(this.eventPrefix, input)}`,
59
+ "group by bucketStart",
60
+ "order by bucketStart asc"
61
+ ].join("\n"),
62
+ values: buildUsageValues(input)
63
+ }));
64
+ }
65
+ async queryHogQL(input) {
66
+ if (!this.reader.queryHogQL) throw new Error("Analytics reader does not support HogQL queries.");
67
+ return this.reader.queryHogQL(input);
68
+ }
69
+ };
70
+ function buildUsageWhereClause(eventPrefix, input) {
71
+ const clauses = [`event = '${eventPrefix}.usage_recorded'`, `properties.metricKey = {metricKey}`];
72
+ if (input.subjectId) clauses.push("distinct_id = {subjectId}");
73
+ if (input.dateRange?.from) clauses.push("timestamp >= {dateFrom}");
74
+ if (input.dateRange?.to) clauses.push("timestamp < {dateTo}");
75
+ return clauses.join(" and ");
76
+ }
77
+ function buildSummaryWhereClause(eventPrefix, input) {
78
+ const clauses = [`event = '${eventPrefix}.usage_aggregated'`, `properties.metricKey = {metricKey}`];
79
+ if (input.subjectId) clauses.push("distinct_id = {subjectId}");
80
+ if (input.periodType) clauses.push("properties.periodType = {periodType}");
81
+ if (input.dateRange?.from) clauses.push("timestamp >= {dateFrom}");
82
+ if (input.dateRange?.to) clauses.push("timestamp < {dateTo}");
83
+ return clauses.join(" and ");
84
+ }
85
+ function buildUsageValues(input) {
86
+ return {
87
+ metricKey: input.metricKey,
88
+ subjectId: input.subjectId,
89
+ dateFrom: toIsoString(input.dateRange?.from),
90
+ dateTo: toIsoString(input.dateRange?.to)
91
+ };
92
+ }
93
+ function buildSummaryValues(input) {
94
+ return {
95
+ metricKey: input.metricKey,
96
+ subjectId: input.subjectId,
97
+ periodType: input.periodType,
98
+ dateFrom: toIsoString(input.dateRange?.from),
99
+ dateTo: toIsoString(input.dateRange?.to)
100
+ };
101
+ }
102
+ function bucketExpression(bucket) {
103
+ switch (bucket) {
104
+ case "hour": return "toStartOfHour(timestamp)";
105
+ case "week": return "toStartOfWeek(timestamp)";
106
+ case "month": return "toStartOfMonth(timestamp)";
107
+ default: return "toStartOfDay(timestamp)";
108
+ }
109
+ }
110
+ function mapUsageRecords(result) {
111
+ return mapRows(result).flatMap((row) => {
112
+ const metricKey = asString(row.metricKey);
113
+ const subjectType = asString(row.subjectType);
114
+ const subjectId = asString(row.subjectId);
115
+ const timestamp = asDate(row.timestamp);
116
+ if (!metricKey || !subjectType || !subjectId || !timestamp) return [];
117
+ return [{
118
+ recordId: asOptionalString(row.recordId),
119
+ metricKey,
120
+ subjectType,
121
+ subjectId,
122
+ quantity: asNumber(row.quantity),
123
+ source: asOptionalString(row.source),
124
+ timestamp
125
+ }];
126
+ });
127
+ }
128
+ function mapUsageSummaries(result) {
129
+ return mapRows(result).flatMap((row) => {
130
+ const metricKey = asString(row.metricKey);
131
+ const subjectType = asString(row.subjectType);
132
+ const subjectId = asString(row.subjectId);
133
+ const periodType = asString(row.periodType);
134
+ const periodStart = asDate(row.periodStart);
135
+ const periodEnd = asDate(row.periodEnd);
136
+ const aggregatedAt = asDate(row.aggregatedAt);
137
+ if (!metricKey || !subjectType || !subjectId || !periodType || !periodStart || !periodEnd || !aggregatedAt) return [];
138
+ return [{
139
+ summaryId: asOptionalString(row.summaryId),
140
+ metricKey,
141
+ subjectType,
142
+ subjectId,
143
+ periodType,
144
+ periodStart,
145
+ periodEnd,
146
+ totalQuantity: asNumber(row.totalQuantity),
147
+ recordCount: asNumber(row.recordCount),
148
+ aggregatedAt
149
+ }];
150
+ });
151
+ }
152
+ function mapUsageTrend(result) {
153
+ return mapRows(result).flatMap((row) => {
154
+ const bucketStart = asString(row.bucketStart);
155
+ if (!bucketStart) return [];
156
+ return [{
157
+ bucketStart,
158
+ totalQuantity: asNumber(row.totalQuantity),
159
+ recordCount: asNumber(row.recordCount)
160
+ }];
161
+ });
162
+ }
163
+ function mapRows(result) {
164
+ if (!Array.isArray(result.results) || !Array.isArray(result.columns)) return [];
165
+ const columns = result.columns;
166
+ return result.results.flatMap((row) => {
167
+ if (!Array.isArray(row)) return [];
168
+ const record = {};
169
+ columns.forEach((column, index) => {
170
+ record[column] = row[index];
171
+ });
172
+ return [record];
173
+ });
174
+ }
175
+ function asString(value) {
176
+ if (typeof value === "string" && value.trim()) return value;
177
+ if (typeof value === "number") return String(value);
178
+ return null;
179
+ }
180
+ function asOptionalString(value) {
181
+ if (typeof value === "string") return value;
182
+ if (typeof value === "number") return String(value);
183
+ }
184
+ function asNumber(value) {
185
+ if (typeof value === "number" && Number.isFinite(value)) return value;
186
+ if (typeof value === "string" && value.trim()) {
187
+ const parsed = Number(value);
188
+ if (Number.isFinite(parsed)) return parsed;
189
+ }
190
+ return 0;
191
+ }
192
+ function asDate(value) {
193
+ if (value instanceof Date) return value;
194
+ if (typeof value === "string" || typeof value === "number") {
195
+ const date = new Date(value);
196
+ if (!Number.isNaN(date.getTime())) return date;
197
+ }
198
+ return null;
199
+ }
200
+ function toIsoString(value) {
201
+ if (!value) return void 0;
202
+ return typeof value === "string" ? value : value.toISOString();
203
+ }
204
+
205
+ //#endregion
206
+ export { PosthogMeteringReader };
207
+ //# sourceMappingURL=posthog-metering-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posthog-metering-reader.js","names":[],"sources":["../../src/analytics/posthog-metering-reader.ts"],"sourcesContent":["import type {\n AnalyticsReader,\n AnalyticsQueryResult,\n DateRangeInput,\n} from '@contractspec/lib.contracts/integrations/providers/analytics';\nimport type {\n MeteringUsageAggregate,\n MeteringUsageRecord,\n} from './posthog-metering';\n\nexport interface UsageByMetricInput {\n metricKey: string;\n dateRange?: DateRangeInput;\n subjectId?: string;\n limit?: number;\n}\n\nexport interface UsageSummaryInput {\n metricKey: string;\n subjectId?: string;\n periodType?: string;\n dateRange?: DateRangeInput;\n limit?: number;\n}\n\nexport type UsageTrendBucket = 'hour' | 'day' | 'week' | 'month';\n\nexport interface UsageTrendInput {\n metricKey: string;\n bucket: UsageTrendBucket;\n dateRange?: DateRangeInput;\n}\n\nexport interface MeteringUsageTrendPoint {\n bucketStart: string;\n totalQuantity: number;\n recordCount: number;\n}\n\nexport interface PosthogMeteringReaderOptions {\n eventPrefix?: string;\n}\n\nexport class PosthogMeteringReader {\n private readonly reader: AnalyticsReader;\n private readonly eventPrefix: string;\n\n constructor(\n reader: AnalyticsReader,\n options: PosthogMeteringReaderOptions = {}\n ) {\n this.reader = reader;\n this.eventPrefix = options.eventPrefix ?? 'metering';\n }\n\n async getUsageByMetric(\n input: UsageByMetricInput\n ): Promise<MeteringUsageRecord[]> {\n const result = await this.queryHogQL({\n query: [\n 'select',\n ' properties.recordId as recordId,',\n ' properties.metricKey as metricKey,',\n ' properties.subjectType as subjectType,',\n ' distinct_id as subjectId,',\n ' properties.quantity as quantity,',\n ' properties.source as source,',\n ' timestamp as timestamp',\n 'from events',\n `where ${buildUsageWhereClause(this.eventPrefix, input)}`,\n 'order by timestamp desc',\n `limit ${input.limit ?? 200}`,\n ].join('\\n'),\n values: buildUsageValues(input),\n });\n\n return mapUsageRecords(result);\n }\n\n async getUsageSummary(\n input: UsageSummaryInput\n ): Promise<MeteringUsageAggregate[]> {\n const result = await this.queryHogQL({\n query: [\n 'select',\n ' properties.summaryId as summaryId,',\n ' properties.metricKey as metricKey,',\n ' properties.subjectType as subjectType,',\n ' distinct_id as subjectId,',\n ' properties.periodType as periodType,',\n ' properties.periodStart as periodStart,',\n ' properties.periodEnd as periodEnd,',\n ' properties.totalQuantity as totalQuantity,',\n ' properties.recordCount as recordCount,',\n ' timestamp as aggregatedAt',\n 'from events',\n `where ${buildSummaryWhereClause(this.eventPrefix, input)}`,\n 'order by timestamp desc',\n `limit ${input.limit ?? 200}`,\n ].join('\\n'),\n values: buildSummaryValues(input),\n });\n\n return mapUsageSummaries(result);\n }\n\n async getUsageTrend(\n input: UsageTrendInput\n ): Promise<MeteringUsageTrendPoint[]> {\n const result = await this.queryHogQL({\n query: [\n 'select',\n ` ${bucketExpression(input.bucket)} as bucketStart,`,\n ' sum(properties.quantity) as totalQuantity,',\n ' count() as recordCount',\n 'from events',\n `where ${buildUsageWhereClause(this.eventPrefix, input)}`,\n 'group by bucketStart',\n 'order by bucketStart asc',\n ].join('\\n'),\n values: buildUsageValues(input),\n });\n\n return mapUsageTrend(result);\n }\n\n private async queryHogQL(input: {\n query: string;\n values: Record<string, unknown>;\n }): Promise<AnalyticsQueryResult> {\n if (!this.reader.queryHogQL) {\n throw new Error('Analytics reader does not support HogQL queries.');\n }\n return this.reader.queryHogQL(input);\n }\n}\n\nfunction buildUsageWhereClause(\n eventPrefix: string,\n input: { dateRange?: DateRangeInput; metricKey: string; subjectId?: string }\n): string {\n const clauses = [\n `event = '${eventPrefix}.usage_recorded'`,\n `properties.metricKey = {metricKey}`,\n ];\n if (input.subjectId) {\n clauses.push('distinct_id = {subjectId}');\n }\n if (input.dateRange?.from) {\n clauses.push('timestamp >= {dateFrom}');\n }\n if (input.dateRange?.to) {\n clauses.push('timestamp < {dateTo}');\n }\n return clauses.join(' and ');\n}\n\nfunction buildSummaryWhereClause(\n eventPrefix: string,\n input: {\n dateRange?: DateRangeInput;\n metricKey: string;\n subjectId?: string;\n periodType?: string;\n }\n): string {\n const clauses = [\n `event = '${eventPrefix}.usage_aggregated'`,\n `properties.metricKey = {metricKey}`,\n ];\n if (input.subjectId) {\n clauses.push('distinct_id = {subjectId}');\n }\n if (input.periodType) {\n clauses.push('properties.periodType = {periodType}');\n }\n if (input.dateRange?.from) {\n clauses.push('timestamp >= {dateFrom}');\n }\n if (input.dateRange?.to) {\n clauses.push('timestamp < {dateTo}');\n }\n return clauses.join(' and ');\n}\n\nfunction buildUsageValues(input: {\n metricKey: string;\n subjectId?: string;\n dateRange?: DateRangeInput;\n}): Record<string, unknown> {\n return {\n metricKey: input.metricKey,\n subjectId: input.subjectId,\n dateFrom: toIsoString(input.dateRange?.from),\n dateTo: toIsoString(input.dateRange?.to),\n };\n}\n\nfunction buildSummaryValues(input: {\n metricKey: string;\n subjectId?: string;\n periodType?: string;\n dateRange?: DateRangeInput;\n}): Record<string, unknown> {\n return {\n metricKey: input.metricKey,\n subjectId: input.subjectId,\n periodType: input.periodType,\n dateFrom: toIsoString(input.dateRange?.from),\n dateTo: toIsoString(input.dateRange?.to),\n };\n}\n\nfunction bucketExpression(bucket: UsageTrendBucket): string {\n switch (bucket) {\n case 'hour':\n return 'toStartOfHour(timestamp)';\n case 'week':\n return 'toStartOfWeek(timestamp)';\n case 'month':\n return 'toStartOfMonth(timestamp)';\n case 'day':\n default:\n return 'toStartOfDay(timestamp)';\n }\n}\n\nfunction mapUsageRecords(result: AnalyticsQueryResult): MeteringUsageRecord[] {\n const rows = mapRows(result);\n return rows.flatMap((row) => {\n const metricKey = asString(row.metricKey);\n const subjectType = asString(row.subjectType);\n const subjectId = asString(row.subjectId);\n const timestamp = asDate(row.timestamp);\n if (!metricKey || !subjectType || !subjectId || !timestamp) {\n return [];\n }\n return [\n {\n recordId: asOptionalString(row.recordId),\n metricKey,\n subjectType,\n subjectId,\n quantity: asNumber(row.quantity),\n source: asOptionalString(row.source),\n timestamp,\n },\n ];\n });\n}\n\nfunction mapUsageSummaries(\n result: AnalyticsQueryResult\n): MeteringUsageAggregate[] {\n const rows = mapRows(result);\n return rows.flatMap((row) => {\n const metricKey = asString(row.metricKey);\n const subjectType = asString(row.subjectType);\n const subjectId = asString(row.subjectId);\n const periodType = asString(row.periodType);\n const periodStart = asDate(row.periodStart);\n const periodEnd = asDate(row.periodEnd);\n const aggregatedAt = asDate(row.aggregatedAt);\n if (\n !metricKey ||\n !subjectType ||\n !subjectId ||\n !periodType ||\n !periodStart ||\n !periodEnd ||\n !aggregatedAt\n ) {\n return [];\n }\n return [\n {\n summaryId: asOptionalString(row.summaryId),\n metricKey,\n subjectType,\n subjectId,\n periodType,\n periodStart,\n periodEnd,\n totalQuantity: asNumber(row.totalQuantity),\n recordCount: asNumber(row.recordCount),\n aggregatedAt,\n },\n ];\n });\n}\n\nfunction mapUsageTrend(\n result: AnalyticsQueryResult\n): MeteringUsageTrendPoint[] {\n const rows = mapRows(result);\n return rows.flatMap((row) => {\n const bucketStart = asString(row.bucketStart);\n if (!bucketStart) return [];\n return [\n {\n bucketStart,\n totalQuantity: asNumber(row.totalQuantity),\n recordCount: asNumber(row.recordCount),\n },\n ];\n });\n}\n\nfunction mapRows(result: AnalyticsQueryResult): Record<string, unknown>[] {\n if (!Array.isArray(result.results) || !Array.isArray(result.columns)) {\n return [];\n }\n const columns = result.columns;\n return result.results.flatMap((row) => {\n if (!Array.isArray(row)) return [];\n const record: Record<string, unknown> = {};\n columns.forEach((column, index) => {\n record[column] = row[index];\n });\n return [record];\n });\n}\n\nfunction asString(value: unknown): string | null {\n if (typeof value === 'string' && value.trim()) return value;\n if (typeof value === 'number') return String(value);\n return null;\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return String(value);\n return undefined;\n}\n\nfunction asNumber(value: unknown): number {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n if (typeof value === 'string' && value.trim()) {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return 0;\n}\n\nfunction asDate(value: unknown): Date | null {\n if (value instanceof Date) return value;\n if (typeof value === 'string' || typeof value === 'number') {\n const date = new Date(value);\n if (!Number.isNaN(date.getTime())) return date;\n }\n return null;\n}\n\nfunction toIsoString(value?: string | Date): string | undefined {\n if (!value) return undefined;\n return typeof value === 'string' ? value : value.toISOString();\n}\n"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { AnalyticsProvider } from "@contractspec/lib.contracts/integrations/providers/analytics";
2
+
3
+ //#region src/analytics/posthog-metering.d.ts
4
+ interface MeteringUsageRecord {
5
+ recordId?: string;
6
+ metricKey: string;
7
+ subjectType: string;
8
+ subjectId: string;
9
+ quantity: number;
10
+ source?: string;
11
+ timestamp: Date;
12
+ }
13
+ interface MeteringUsageAggregate {
14
+ summaryId?: string;
15
+ metricKey: string;
16
+ subjectType: string;
17
+ subjectId: string;
18
+ periodType: string;
19
+ periodStart: Date;
20
+ periodEnd: Date;
21
+ totalQuantity: number;
22
+ recordCount: number;
23
+ aggregatedAt: Date;
24
+ }
25
+ interface PosthogMeteringReporterOptions {
26
+ eventPrefix?: string;
27
+ includeSource?: boolean;
28
+ }
29
+ declare class PosthogMeteringReporter {
30
+ private readonly provider;
31
+ private readonly eventPrefix;
32
+ private readonly includeSource;
33
+ constructor(provider: AnalyticsProvider, options?: PosthogMeteringReporterOptions);
34
+ captureUsageRecorded(record: MeteringUsageRecord): Promise<void>;
35
+ captureUsageAggregated(summary: MeteringUsageAggregate): Promise<void>;
36
+ }
37
+ //#endregion
38
+ export { MeteringUsageAggregate, MeteringUsageRecord, PosthogMeteringReporter, PosthogMeteringReporterOptions };
39
+ //# sourceMappingURL=posthog-metering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posthog-metering.d.ts","names":[],"sources":["../../src/analytics/posthog-metering.ts"],"mappings":";;;UAEiB,mBAAA;EACf,QAAA;EACA,SAAA;EACA,WAAA;EACA,SAAA;EACA,QAAA;EACA,MAAA;EACA,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,sBAAA;EACf,SAAA;EACA,SAAA;EACA,WAAA;EACA,SAAA;EACA,UAAA;EACA,WAAA,EAAa,IAAA;EACb,SAAA,EAAW,IAAA;EACX,aAAA;EACA,WAAA;EACA,YAAA,EAAc,IAAA;AAAA;AAAA,UAGC,8BAAA;EACf,WAAA;EACA,aAAA;AAAA;AAAA,cAGW,uBAAA;EAAA,iBACM,QAAA;EAAA,iBACA,WAAA;EAAA,iBACA,aAAA;cAGf,QAAA,EAAU,iBAAA,EACV,OAAA,GAAS,8BAAA;EAOL,oBAAA,CAAqB,MAAA,EAAQ,mBAAA,GAAsB,OAAA;EAiBnD,sBAAA,CAAuB,OAAA,EAAS,sBAAA,GAAyB,OAAA;AAAA"}
@@ -0,0 +1,46 @@
1
+ //#region src/analytics/posthog-metering.ts
2
+ var PosthogMeteringReporter = class {
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
+
44
+ //#endregion
45
+ export { PosthogMeteringReporter };
46
+ //# sourceMappingURL=posthog-metering.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posthog-metering.js","names":[],"sources":["../../src/analytics/posthog-metering.ts"],"sourcesContent":["import type { AnalyticsProvider } from '@contractspec/lib.contracts/integrations/providers/analytics';\n\nexport interface MeteringUsageRecord {\n recordId?: string;\n metricKey: string;\n subjectType: string;\n subjectId: string;\n quantity: number;\n source?: string;\n timestamp: Date;\n}\n\nexport interface MeteringUsageAggregate {\n summaryId?: string;\n metricKey: string;\n subjectType: string;\n subjectId: string;\n periodType: string;\n periodStart: Date;\n periodEnd: Date;\n totalQuantity: number;\n recordCount: number;\n aggregatedAt: Date;\n}\n\nexport interface PosthogMeteringReporterOptions {\n eventPrefix?: string;\n includeSource?: boolean;\n}\n\nexport class PosthogMeteringReporter {\n private readonly provider: AnalyticsProvider;\n private readonly eventPrefix: string;\n private readonly includeSource: boolean;\n\n constructor(\n provider: AnalyticsProvider,\n options: PosthogMeteringReporterOptions = {}\n ) {\n this.provider = provider;\n this.eventPrefix = options.eventPrefix ?? 'metering';\n this.includeSource = options.includeSource ?? true;\n }\n\n async captureUsageRecorded(record: MeteringUsageRecord): Promise<void> {\n await this.provider.capture({\n distinctId: record.subjectId,\n event: `${this.eventPrefix}.usage_recorded`,\n timestamp: record.timestamp,\n properties: {\n recordId: record.recordId ?? null,\n metricKey: record.metricKey,\n subjectType: record.subjectType,\n quantity: record.quantity,\n ...(this.includeSource && record.source\n ? { source: record.source }\n : {}),\n },\n });\n }\n\n async captureUsageAggregated(summary: MeteringUsageAggregate): Promise<void> {\n await this.provider.capture({\n distinctId: summary.subjectId,\n event: `${this.eventPrefix}.usage_aggregated`,\n timestamp: summary.aggregatedAt,\n properties: {\n summaryId: summary.summaryId ?? null,\n metricKey: summary.metricKey,\n subjectType: summary.subjectType,\n periodType: summary.periodType,\n periodStart: summary.periodStart.toISOString(),\n periodEnd: summary.periodEnd.toISOString(),\n totalQuantity: summary.totalQuantity,\n recordCount: summary.recordCount,\n },\n });\n }\n}\n"],"mappings":";AA8BA,IAAa,0BAAb,MAAqC;CACnC,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CAEjB,YACE,UACA,UAA0C,EAAE,EAC5C;AACA,OAAK,WAAW;AAChB,OAAK,cAAc,QAAQ,eAAe;AAC1C,OAAK,gBAAgB,QAAQ,iBAAiB;;CAGhD,MAAM,qBAAqB,QAA4C;AACrE,QAAM,KAAK,SAAS,QAAQ;GAC1B,YAAY,OAAO;GACnB,OAAO,GAAG,KAAK,YAAY;GAC3B,WAAW,OAAO;GAClB,YAAY;IACV,UAAU,OAAO,YAAY;IAC7B,WAAW,OAAO;IAClB,aAAa,OAAO;IACpB,UAAU,OAAO;IACjB,GAAI,KAAK,iBAAiB,OAAO,SAC7B,EAAE,QAAQ,OAAO,QAAQ,GACzB,EAAE;IACP;GACF,CAAC;;CAGJ,MAAM,uBAAuB,SAAgD;AAC3E,QAAM,KAAK,SAAS,QAAQ;GAC1B,YAAY,QAAQ;GACpB,OAAO,GAAG,KAAK,YAAY;GAC3B,WAAW,QAAQ;GACnB,YAAY;IACV,WAAW,QAAQ,aAAa;IAChC,WAAW,QAAQ;IACnB,aAAa,QAAQ;IACrB,YAAY,QAAQ;IACpB,aAAa,QAAQ,YAAY,aAAa;IAC9C,WAAW,QAAQ,UAAU,aAAa;IAC1C,eAAe,QAAQ;IACvB,aAAa,QAAQ;IACtB;GACF,CAAC"}