diff --git a/src/ingestion/connectors/ai/claude-team/connector.yaml b/src/ingestion/connectors/ai/claude-team/connector.yaml index b4c156310..d56389e3b 100644 --- a/src/ingestion/connectors/ai/claude-team/connector.yaml +++ b/src/ingestion/connectors/ai/claude-team/connector.yaml @@ -255,9 +255,18 @@ streams: - type: AddedFieldDefinition path: [source_id] value: "{{ config['insight_source_id'] }}" + # The month belongs in the key. This endpoint reports the billing + # month in progress and carries no history, so a month exists only in + # the rows read during it. Keyed on the seat alone, Bronze kept one + # row per seat — its latest state — and every closed month became + # unreproducible: a `dbt --full-refresh` (dispatched by a MAJOR + # descriptor bump) rebuilt Silver from that residue and silently + # dropped the history. Keyed on seat-and-month, Bronze retains the + # closing state of each month, which is the grain Silver already + # keeps, and a rebuild reproduces it. - type: AddedFieldDefinition path: [unique_key] - value: "{{ config['insight_tenant_id'] }}-{{ config['insight_source_id'] }}-{{ record['account_uuid'] }}" + value: "{{ config['insight_tenant_id'] }}-{{ config['insight_source_id'] }}-{{ record['account_uuid'] }}-{{ now_utc().strftime('%Y-%m') }}" - type: AddedFieldDefinition path: [collected_at] value: "{{ now_utc().strftime('%Y-%m-%dT%H:%M:%SZ') }}" diff --git a/src/ingestion/connectors/ai/claude-team/dbt/claude_team__ai_overage.sql b/src/ingestion/connectors/ai/claude-team/dbt/claude_team__ai_overage.sql index 57d0e69a5..749451003 100644 --- a/src/ingestion/connectors/ai/claude-team/dbt/claude_team__ai_overage.sql +++ b/src/ingestion/connectors/ai/claude-team/dbt/claude_team__ai_overage.sql @@ -40,9 +40,10 @@ ) }} WITH latest_per_seat_month AS ( - -- Bronze is full-refresh+append: every sync re-emits all seats with the - -- same unique_key (no date). Collapse to the latest snapshot per seat per - -- calendar month so each (seat, month) keeps its freshest spend reading. + -- Bronze is full-refresh+append and its unique_key carries the extraction + -- month, so it keeps each seat's closing state per month. Collapsing here + -- is still required: repeated syncs within a month re-emit the seat, and + -- rows written before the key carried a month have no month in theirs. SELECT * FROM {{ source('bronze_claude_team', 'claude_team_overage_spend') }} WHERE account_uuid IS NOT NULL @@ -60,9 +61,11 @@ WITH latest_per_seat_month AS ( SELECT tenant_id AS insight_tenant_id, source_id, - -- Silver dedup key: tenant-source-seat-month. The month component (absent - -- from the Bronze unique_key) preserves monthly history and gives intra- - -- month idempotency (latest snapshot wins via _version, same key). + -- Silver dedup key: tenant-source-seat-month, the same grain Bronze now + -- keys on. Derived here from the extraction timestamp rather than copied, + -- so rows written under the older key still land in the right month. The + -- month preserves history and gives intra-month idempotency (latest + -- snapshot wins via _version, same key). CAST(concat( coalesce(tenant_id, ''), '-', coalesce(source_id, ''), '-', diff --git a/src/ingestion/connectors/ai/claude-team/descriptor.yaml b/src/ingestion/connectors/ai/claude-team/descriptor.yaml index 4013a873a..8520d15ec 100644 --- a/src/ingestion/connectors/ai/claude-team/descriptor.yaml +++ b/src/ingestion/connectors/ai/claude-team/descriptor.yaml @@ -15,7 +15,14 @@ name: claude-team # conversation_count and declares tool_label — MAJOR per ADR-0015: # pre-existing rows must re-materialize from Bronze to populate the new # columns (one-shot scoped full refresh). -version: "2.0.0" +# 2.1.0: claude_team_overage_spend's unique_key gains the extraction month, so +# Bronze retains one row per seat per billing month instead of one per seat. +# MINOR per ADR-0015 — additive: existing rows keep their keys and stay valid, +# nothing has to re-materialize. Deliberately NOT major: a major bump +# dispatches a one-shot `dbt --full-refresh` (reconcile-connectors), which +# rebuilds Silver from Bronze — the very operation this change exists to make +# safe, and the one that cost the June history. +version: "2.1.0" # Daily at 04:00 UTC — off-peak vs other connectors. The customer's # proxy runs a single browser instance, so we avoid scheduling # claude-team next to anything else that may also hit it.