Skip to content
15 changes: 11 additions & 4 deletions src/ingestion/connectors/ai/claude-team/descriptor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@ name: claude-team
# Rows written before this version keep an empty seat_status and reach gold as
# 'unknown', which is what they honestly are.
version: "2.2.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.
schedule: '0 4 * * *'
# Daily at 23:50 UTC, as late in the day as the boundary allows. The seat
# endpoint reports the billing month in progress and carries no history, so a
# month's spend is frozen at the last read INSIDE it and whatever follows is
# lost for good: reading at 04:00 dropped the last twenty hours of every month.
# A run after midnight cannot make up for it — by then the endpoint answers for
# the new month.
#
# Still off-peak vs other connectors, which occupy 02:00 to 06:00 and 11:00:
# the customer's proxy runs a single browser instance, so claude-team is not
# scheduled next to anything else that may hit it.
schedule: '50 23 * * *'
workflow: sync

dbt_select: 'tag:claude-team+'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{{ config(
tags=['data_quality'],
severity='warn',
store_failures=true,
meta={
'title': 'The month that just closed was read close to its end',
'domain': 'ai',
'category': 'coverage',
'tier': 'error',
'remediation': 'A row here is a connector instance whose last seat-spend read of the month that just closed happened hours before the month ended. The seat endpoint reports the month in progress and carries no history, so everything spent after that read is lost permanently — a re-sync cannot recover it, because the endpoint now answers for the current month. Check that the sync ran on the last day of the month and succeeded: the schedule is meant to place the final read minutes before the boundary, and a single failed run there costs a day of spend. Past rows do not clear; they record a month that is already short.'
}
) }}
{#- Only the month that has just closed is judged. An open month is still
being read, and an older one cannot be acted on any more than it can be
re-read, so flagging either would produce noise nobody can clear.

Six hours is the line: the schedule puts the last read ten minutes before
the boundary, and one failed run on the last day makes it a full day. -#}

WITH toStartOfMonth(today()) AS current_month,
current_month - INTERVAL 1 MONTH AS closed_month

{# `closed_month` is NOT aliased to `period_month` below: a SELECT alias
shadows the column of the same name, the WHERE would compare the alias
with itself, and every month would collapse into one group whose latest
read is the open month's — silently inverting the check.

Untrimmed delimiters on purpose: this comment stands between two SQL
tokens, and a trimming one would glue `closed_month` to `SELECT`. #}
SELECT
insight_tenant_id,
source_id,
source,
closed_month AS billing_month,
max(collected_at) AS last_read,
dateDiff('hour', max(collected_at), toDateTime(current_month)) AS hours_unread
FROM {{ ref('class_ai_overage') }} FINAL
WHERE period_month = closed_month
GROUP BY
insight_tenant_id,
source_id,
source
HAVING hours_unread > 6
Loading