59 lines · 2.3 KB
import { first, run } from "../db";
import { accountDb } from "../scoped-db";

const DAY_MS = 86_400_000;
const ISO_DATE_LENGTH = 10;

/**
 * Mirrors the usage-metering `UsageInput` for meter `ops.integration_deliveries` (one unit per outbound
 * delivery, keyed `integration:<deliveryId>` so retries never double-count). Once roadmap/metering lands the
 * body becomes `recordUsageSoon(null, { accountId: ownerId, repoId, meter: "ops.integration_deliveries",
 * quantity: 1, attributes: { kind, ok, bytes }, idempotencyKey })`. Until then it sums into
 * `integration_usage_daily`, which billing does not read.
 */
export type OutboundUsage = {
  ownerId: string;
  repoId: string | null;
  kind: string;
  ok: boolean;
  bytes: number;
  deliveryId: string;
};

export async function recordOutboundDelivery(usage: OutboundUsage, now = Date.now()): Promise<void> {
  const day = new Date(Math.floor(now / DAY_MS) * DAY_MS).toISOString().slice(0, ISO_DATE_LENGTH);
  try {
    const claimed = await run(
      "INSERT OR IGNORE INTO integration_usage_keys (key, created_at) VALUES (?, ?)",
      `integration:${usage.deliveryId}`,
      now,
    );
    if (!claimed.meta.changes) return;
    await accountDb(usage.ownerId).run(
      `INSERT INTO integration_usage_daily (owner_id, day, kind, deliveries, failures, bytes) VALUES (?, ?, ?, 1, ?, ?)
       ON CONFLICT(owner_id, day, kind) DO UPDATE SET deliveries = deliveries + 1,
         failures = failures + excluded.failures, bytes = bytes + excluded.bytes`,
      usage.ownerId,
      day,
      usage.kind,
      usage.ok ? 0 : 1,
      Math.max(0, Math.floor(usage.bytes)),