Skip to content

fix(m365): treat unknown Teams license as unlicensed; adopt semver - #1875

Merged
mitasovr merged 3 commits into
constructorfabric:mainfrom
mitasovr:claude/m365-736-strict-license-and-semver
Jul 23, 2026
Merged

fix(m365): treat unknown Teams license as unlicensed; adopt semver#1875
mitasovr merged 3 commits into
constructorfabric:mainfrom
mitasovr:claude/m365-736-strict-license-and-semver

Conversation

@mitasovr

@mitasovr mitasovr commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #736 (shipped in #1869), addressing the CodeRabbit review on that PR.

1. Teams license filter — exclude unknown (NULL) too

CodeRabbit flagged that the merged Teams filter kept isLicensed IS NULL rows instead of excluding them as the issue proposed. Switched both Teams feeders:

- AND coalesce(isLicensed, true) = true
+ AND coalesce(isLicensed, false) = true

Now an unlicensed or unknown-license row is dropped; only an explicit isLicensed=true row is kept (m365__collab_meeting_activity, m365__collab_chat_activity).

  • The e2e teams fixture template now defaults isLicensed: true so existing fixtures stay counted.
  • The regression test collab_meeting_unlicensed_excluded.test.yaml gains an isLicensed=null case asserting exclusion (alice true→3, frank false→null, grace null→null).

The email/onedrive/sharepoint feeders keep their conservative assignedProducts filter (drop only explicitly-empty lists, keep NULL): those Graph reports expose no isLicensed, and the JSON-string serialization is inferred rather than verified, so a NULL there means "unknown/unsynced" and must not nuke the whole feeder.

2. descriptor.yaml → strict semver (ADR-0015), step 1 of 2

CodeRabbit also noted the incremental models don't retro-delete rows materialized before the #736 filter. The blessed cleanup per ADR-0015 is a MAJOR-bump full-refresh over dbt_select (tag:m365+) — but that only fires when the current deployed version is already semver.

m365 is currently version: "2026.05.04" (non-semver), so classify_bump.py would treat any bump as a migration (republish + re-discover, no full-refresh). This PR is step 1: migrate to 1.0.0 to establish the semver baseline. A step 2 PR (1.0.0 → 2.0.0) will then dispatch the one-shot dbt --full-refresh that purges the pre-#736 unlicensed rows.

Testing

Dockerized e2e rig, affected Teams metrics:

4 passed, 140 deselected in 290.30s

(new 3-way exclusion test + collab_meetings / collab_messages / collab_meeting_free_days)

Refs #736. Follows #1869.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • M365 collaboration chat and meeting activity now excludes records where licensing status is unknown or explicitly unlicensed.
    • Standard Teams activity test data now defaults to licensed status, improving consistency in reporting.
  • Tests

    • Expanded coverage verifies that both unlicensed and unknown-license users are excluded from meeting metrics.
  • Other

    • Updated the M365 connector version to 1.0.0.

Follow-up to constructorfabric#736 (shipped in constructorfabric#1869), addressing the CodeRabbit review.

1. Teams feeders (meeting, chat): switch `coalesce(isLicensed, true)` to
   `coalesce(isLicensed, false) = true`, so rows with a NULL/unknown license
   flag are dropped alongside explicit `isLicensed=false` — matching the constructorfabric#736
   proposal. The e2e teams fixture template now defaults `isLicensed: true` so
   existing fixtures stay counted; the regression test adds an `isLicensed=null`
   case asserting it is excluded.

   The email/onedrive/sharepoint feeders keep their conservative
   `assignedProducts` filter (drop only explicitly-empty lists, keep NULL): those
   reports expose no `isLicensed`, and the JSON-string serialization is inferred
   rather than verified, so a NULL there means "unknown/unsynced" and must not
   nuke the whole feeder.

2. descriptor.yaml: migrate version "2026.05.04" -> "1.0.0" (strict semver,
   ADR-0015). Because the current deployed value is non-semver, reconcile
   classifies this as a `migration` (republish + catalog re-discover, NO
   full-refresh) — it only establishes the semver baseline. A follow-up MAJOR
   bump (1.0.0 -> 2.0.0) will dispatch the one-shot `dbt --full-refresh` over
   `dbt_select` (tag:m365+) that purges collab rows materialized before the constructorfabric#736
   filter landed, since incremental models do not retro-delete already-written
   rows.

Refs constructorfabric#736.

Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mitasovr
mitasovr requested a review from a team as a code owner July 23, 2026 12:35
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

M365 collaboration chat and meeting models now treat NULL isLicensed values as unlicensed. The connector version is set to 1.0.0, shared fixtures default to licensed, and end-to-end tests verify exclusion of explicit and unknown unlicensed users.

Changes

M365 license filtering

Layer / File(s) Summary
License filtering and connector version
src/ingestion/connectors/collaboration/m365/dbt/*, src/ingestion/connectors/collaboration/m365/descriptor.yaml
Chat and meeting activity models now exclude NULL isLicensed records, and the connector descriptor version is 1.0.0.
Fixture defaults and exclusion assertions
src/ingestion/tests/e2e/metrics/templates/m365_teams.yaml, src/ingestion/tests/e2e/metrics/collab_meeting_unlicensed_excluded.test.yaml
Shared Teams fixtures default to isLicensed: true; the meeting metrics test verifies that false and NULL license values are excluded.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: ktursunov

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the license-filtering change and the semver version bump.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
src/ingestion/connectors/collaboration/m365/descriptor.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

src/ingestion/tests/e2e/metrics/collab_meeting_unlicensed_excluded.test.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

src/ingestion/tests/e2e/metrics/templates/m365_teams.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/ingestion/connectors/collaboration/m365/dbt/m365__collab_chat_activity.sql (1)

75-77: 🗄️ Data Integrity & Integration | 🔵 Trivial

Ensure the staged version migration actually completes the data cleanup.

The new predicates only affect rows processed after deployment; existing unlicensed rows remain in these incremental models. The documented 1.0.0 → 2.0.0 full refresh must therefore be guaranteed.

  • src/ingestion/connectors/collaboration/m365/dbt/m365__collab_chat_activity.sql#L75-L77: retain the explicit-true filter and ensure historical rows are refreshed or deleted.
  • src/ingestion/connectors/collaboration/m365/dbt/m365__collab_meeting_activity.sql#L50-L52: retain the explicit-true filter and ensure historical rows are refreshed or deleted.
  • src/ingestion/connectors/collaboration/m365/descriptor.yaml#L2-L8: enforce the subsequent major-version full-refresh rollout.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/ingestion/connectors/collaboration/m365/dbt/m365__collab_chat_activity.sql`
around lines 75 - 77, Retain the explicit-true licensing filters in
m365__collab_chat_activity.sql (75-77) and m365__collab_meeting_activity.sql
(50-52), and ensure historical rows are refreshed or deleted. Update
src/ingestion/connectors/collaboration/m365/descriptor.yaml (2-8) to enforce the
documented 1.0.0-to-2.0.0 full-refresh rollout so the cleanup completes for
existing data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ingestion/connectors/collaboration/m365/descriptor.yaml`:
- Around line 2-8: Update the operator-facing reconcile baseline in ingestion
README documentation from 2026.05.04 to 1.0.0, keeping it consistent with the
descriptor’s version and preserving the surrounding migration guidance.

---

Nitpick comments:
In
`@src/ingestion/connectors/collaboration/m365/dbt/m365__collab_chat_activity.sql`:
- Around line 75-77: Retain the explicit-true licensing filters in
m365__collab_chat_activity.sql (75-77) and m365__collab_meeting_activity.sql
(50-52), and ensure historical rows are refreshed or deleted. Update
src/ingestion/connectors/collaboration/m365/descriptor.yaml (2-8) to enforce the
documented 1.0.0-to-2.0.0 full-refresh rollout so the cleanup completes for
existing data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44e35fa4-c23b-4005-8c93-4bc88f809955

📥 Commits

Reviewing files that changed from the base of the PR and between 3214e31 and 803fe95.

📒 Files selected for processing (5)
  • src/ingestion/connectors/collaboration/m365/dbt/m365__collab_chat_activity.sql
  • src/ingestion/connectors/collaboration/m365/dbt/m365__collab_meeting_activity.sql
  • src/ingestion/connectors/collaboration/m365/descriptor.yaml
  • src/ingestion/tests/e2e/metrics/collab_meeting_unlicensed_excluded.test.yaml
  • src/ingestion/tests/e2e/metrics/templates/m365_teams.yaml

Comment on lines +2 to +8
# 1.0.0 (semver migration): adopt strict MAJOR.MINOR.PATCH (ADR-0015). The prior
# value "2026.05.04" is not semver, so reconcile classifies current→1.0.0 as a
# `migration` (republish + catalog re-discover, NO full-refresh). This only
# establishes the semver baseline. A follow-up MAJOR bump (1.0.0 → 2.0.0) will
# dispatch the one-shot `dbt --full-refresh` over `dbt_select` (tag:m365+) that
# purges collab rows materialized before the #736 unlicensed-user filter landed.
version: "1.0.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the documented reconcile baseline.

src/ingestion/README.md still describes 2026.05.04 as the current baseline, which will contradict this descriptor’s 1.0.0 version after merge. Update the operator-facing documentation in the same change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ingestion/connectors/collaboration/m365/descriptor.yaml` around lines 2 -
8, Update the operator-facing reconcile baseline in ingestion README
documentation from 2026.05.04 to 1.0.0, keeping it consistent with the
descriptor’s version and preserving the surrounding migration guidance.

@mitasovr
mitasovr enabled auto-merge (squash) July 23, 2026 12:54
# coalesce(isLicensed, false) != true, so an unset flag would otherwise
# exclude every fixture row. Tests exercising the unlicensed path set this
# explicitly to false (or null). See #736.
isLicensed: true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's check all cases - true/false/null

@mitasovr
mitasovr merged commit 23d39e7 into constructorfabric:main Jul 23, 2026
34 checks passed
mitasovr added a commit that referenced this pull request Jul 27, 2026
)

Step 2 of the #736 cleanup. 1.0.0 (#1875) established the semver baseline as a
`migration` with no data action. This MAJOR bump makes reconcile dispatch a
one-shot `dbt --full-refresh` over `dbt_select` (tag:m365+) per ADR-0015, which
re-materializes the m365 staging + downstream silver from append-only bronze and
thereby purges the unlicensed collab rows that incremental models left behind
before the #736 filter landed.

PREREQUISITE: deploy only after the 1.0.0 release has reconciled to the live
environment. If the Airbyte-side current version is still the legacy non-semver
"2026.05.04", classify_bump treats this as `migration` and NO full-refresh is
dispatched.

Refs #736.

Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
Co-authored-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M365 silver: unlicensed users (isLicensed=false) leak into class_collab_meeting_activity

2 participants