Skip to content

fix(source-granola): stop dropping notes at incremental slice boundaries + add acceptance tests - #84277

Draft
devin-ai-integration[bot] wants to merge 3 commits into
masterfrom
devin/1786498096-granola-incremental-boundary
Draft

fix(source-granola): stop dropping notes at incremental slice boundaries + add acceptance tests#84277
devin-ai-integration[bot] wants to merge 3 commits into
masterfrom
devin/1786498096-granola-incremental-boundary

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

What

Related to https://github.com/airbytehq/airbyte-internal-issues/issues/16923:

Two scoped items from the source-granola certification epic — criterion I-2 (incremental correctness) and its prerequisite R-1 (standard tests never exercised the read path). Everything else in the epic (error handlers, backoff, airbyte_type, new streams, updated_after cursor, metadata/support-level, docs, CODEOWNERS) is out of scope and stays with the other sub-issues.

I-2 — notes on 30-day slice boundary dates were silently dropped. The notes cursor used datetime_format: "%Y-%m-%d" with cursor_granularity: P1D, so with step: P30D an intermediate slice was emitted as created_after=2025-10-12&created_before=2025-11-10 and the next slice started at 2025-11-11. Granola's created_before=<bare date> is exclusive with respect to that whole day (verified in the epic: that exact two-parameter probe returns 0 records on a day holding 2 notes), so every boundary day fell into an unqueried gap. Reproduced in the epic: start_date=2025-10-12 emitted 133 of 142 notes, and the 9 missing notes fall exactly on the four boundary dates. Syncs reported success while dropping records.

R-1 — the connector had no acceptance-test-config.yml, so check, discover, basic_read and fail_read_with_bad_catalog all skipped while the suite still exited 0 (2 passed, 6 skipped). That is why the record loss survived 11 releases.

How

Cursor bounds become full timestamps at second granularity:

cursor_datetime_formats: ["%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%d"]
datetime_format: "%Y-%m-%dT%H:%M:%SZ"   # was "%Y-%m-%d"
cursor_granularity: PT1S                # was P1D
end_datetime: now_utc().strftime('%Y-%m-%dT%H:%M:%SZ')

so the first slice now ends at 2025-11-10T23:59:59Z and the next starts at 2025-11-11T00:00:00Z — no gap. The API's published OpenAPI spec accepts date-time for created_after/created_before (https://docs.granola.ai/api-reference/list-notes).

Two compatibility details are deliberate and are what keeps this non-breaking:

  • start_datetime is left date-only. start_date is a user-facing spec field pattern-validated as YYYY-MM-DD, and MinMaxDatetime parses the config value with its own datetime_format. Existing configs keep working untouched; the slice bounds are formatted with the cursor's datetime_format, so nothing is truncated.
  • "%Y-%m-%d" is added to cursor_datetime_formats. CustomFormatConcurrentStreamStateConverter builds its input parse formats from cursor_datetime_formats + datetime_format only, so without this entry, state persisted by ≤0.2.11 (values like "2026-01-09") would fail with No format in [...] matching 2026-01-09. Covered by a test.

New acceptance-test-config.yml follows the current nested convention used by comparable manifest-only sources (closest template: source-plaid) and enables spec, connection, discovery, basic_read, incremental and full_refresh — no bypass_reason, no empty_streams. Supporting fixtures: invalid_config.json, abnormal_state.json, a configured_catalog_incremental.json for the incremental test, and a corrected configured_catalog.json (it previously declared only notes with cursor_field: ["updated_at"], a field that is not in the declared notes schema; it now covers both streams). CI can fetch SECRET_SOURCE-GRANOLA__CREDS per metadata.yaml, so the acceptance tests run there; they cannot run locally (no secret, airbyte-ci unavailable in the dev box).

Declarative-First Evaluation

Fully declarative — the change is four lines inside the existing DatetimeBasedCursor. No components.py exists and none was added.

Test Coverage

New unit_tests/ mock-server suite (modelled on source-harvest, the established manifest-only precedent: unit_tests/pyproject.toml + poetry.lock + CDK manifest_only_fixtures), 3 tests passing:

  1. test_incremental_requests_cover_each_partition — frozen clock, a range spanning three P30D partitions, and exact page_size/created_after/created_before matching for each (no permissive query matching), asserting the boundary day is covered with no gap.
  2. test_boundary_date_record_is_emitted — a note created 2025-11-10T12:00:00Z is emitted.
  3. test_legacy_date_only_state_is_parsed_and_reformatted — incoming {"created_at": "2025-11-10"} (the ≤0.2.11 shape) syncs without error and the newly emitted state is 2025-11-11T00:00:00Z.

Verified they fail on the pre-fix manifest: tests 1 and 3 fail with NoMockAddress: GET .../v1/notes?page_size=30&created_after=2025-10-12&created_before=2025-11-10 (the truncated bounds), and test 2 fails with assert [] == ['boundary-note'] — i.e. the dropped record.

Breaking change evaluation

Not breaking. No spec change (no field added/removed/renamed; start_date still accepts the same values), no schema, primary-key or cursor-field change, no stream removed, and no reduction in the records a stream returns — the change only adds back records that were being dropped. The state format widens from 2026-01-09 to 2026-01-09T00:00:00Z; old values still parse via cursor_datetime_formats, so no state migration is required. Version bumped 0.2.110.2.12 (patch); progressive rollout is disabled for this connector, so no -rc suffix.

Review guide

  1. airbyte-integrations/connectors/source-granola/manifest.yaml — the fix (4 changed lines in notes.incremental_sync)
  2. airbyte-integrations/connectors/source-granola/unit_tests/test_notes.py — asserted slice bounds
  3. airbyte-integrations/connectors/source-granola/acceptance-test-config.yml + integration_tests/ fixtures
  4. metadata.yaml, docs/integrations/sources/granola.md

User Impact

Notes created on a 30-day window boundary date are no longer skipped. Users on existing connections start receiving the previously-missed notes from the next sync onward for boundary dates at or after their current cursor position; notes already dropped in past syncs are not backfilled — recovering those needs a full refresh (or a start_date/state reset). No reconfiguration is required and existing saved state keeps working.

51 connections are on this connector (21 enabled), so please review the state-compatibility reasoning above with that blast radius in mind.

Can this PR be safely reverted and rolled back?

  • YES 💚
  • NO ❌

Reverting restores the previous (lossy) slicing. State written by 0.2.12 is second-precision ISO, which 0.2.11 also parses via its existing cursor_datetime_formats, so a rollback does not break state.

Link to Devin session: https://app.devin.ai/sessions/b1a2b93211fc4ee9a055cfa0ba7ae9ae

devin-ai-integration Bot and others added 2 commits August 12, 2026 01:33
Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Co-Authored-By: bot_apk <apk@cognition.ai>
@github-actions

Copy link
Copy Markdown
Contributor

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

PR Slash Commands

Airbyte Maintainers (that's you!) can execute the following slash commands on your PR:

  • 🛠️ Quick Fixes
    • /format-fix - Fixes most formatting issues.
    • /bump-version - Bumps connector versions, scraping changelog description from the PR title.
      • Bump types: patch (default), minor, major, major_rc, rc, promote.
      • The rc type is a smart default: applies minor_rc if stable, or bumps the RC number if already RC.
      • The promote type strips the RC suffix to finalize a release.
      • Example: /bump-version type=rc or /bump-version type=minor
    • /bump-progressive-rollout-version - Alias for /bump-version type=rc. Bumps with an RC suffix and enables progressive rollout.
  • ❇️ AI Testing and Review (internal link: AI-SDLC Docs):
    • /ai-prove-fix - Runs prerelease readiness checks, including testing against customer connections.
    • /ai-canary-prerelease - Rolls out prerelease to 5-10 connections for canary testing.
    • /ai-review - AI-powered PR review for connector safety and quality gates.
  • 📝 AI Documentation:
    • /ai-docs-review - AI-powered documentation review for PRs with connector changes.
    • /ai-create-docs-pr - Creates a documentation PR for connector changes, stacked on the current PR.
  • 🚀 Connector Releases:
    • /publish-connectors-prerelease - Publishes pre-release connector builds (tagged as {version}-preview.{git-sha}) for all modified connectors in the PR.
    • /enable-autopilot-rollouts - Enables autopilot progressive rollouts for the modified connector(s) in the PR, remediating "autopilot rollouts not enabled for {connector-name}" auto-merge blockers. Sets defaultRolloutMode: autopilot and enableProgressiveRollout: true, preserving any existing autopilotConfig.
      • Optional args: connector=<CONNECTOR_NAME> (defaults to the modified connectors in the PR), strategy=fast|slow|default (defaults to fast).
      • Example: /enable-autopilot-rollouts or /enable-autopilot-rollouts connector=source-faker strategy=slow
  • ☕️ JVM connectors:
    • /update-connector-cdk-version connector=<CONNECTOR_NAME> - Updates the specified connector to the latest CDK version.
      Example: /update-connector-cdk-version connector=destination-bigquery
  • 🐍 Python connectors:
    • /poe connector source-example lock - Run the Poe lock task on the source-example connector, committing the results back to the branch.
    • /poe source example lock - Alias for /poe connector source-example lock.
    • /poe source example use-cdk-branch my/branch - Pin the source-example CDK reference to the branch name specified.
    • /poe source example use-cdk-latest - Update the source-example CDK dependency to the latest available version.
  • ⚙️ Admin commands:
    • /force-merge reason="<REASON>" - Force merges the PR using admin privileges, bypassing CI checks. Requires a reason.
      Example: /force-merge reason="CI is flaky, tests pass locally"
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

@github-actions

Copy link
Copy Markdown
Contributor

Note

Autopilot progressive rollouts are not enabled for the following modified connector(s):

  • source-granola

This is a courtesy heads-up only — it does not block merge or fail any check.
To enable automatic progressive rollouts for the connector(s) above, comment
/enable-autopilot-rollouts on this PR. This sets defaultRolloutMode: autopilot
and enableProgressiveRollout: true in each connector's metadata.yaml,
preserving any existing autopilotConfig.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Deploy preview for airbyte-docs ready!

Project:airbyte-docs
Status: ✅  Deploy successful!
Preview URL:https://airbyte-docs-2fu8wxpbo-airbyte-growth.vercel.app
Latest Commit:bb8078c

Deployed with vercel-action

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

source-granola Connector Test Results

8 tests   5 ✅  9s ⏱️
2 suites  2 💤
2 files    1 ❌

For more details on these failures, see this check.

Results for commit bb8078c.

♻️ This comment has been updated with latest results.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Heads-up for reviewers: the newly enabled acceptance tests cannot pass yet because the test credentials this connector's metadata.yaml declares do not appear to exist in the GSM store.

The source-granola Connector Test Results job fails at secret fetch, before any test logic runs:

Run airbyte-ops secrets fetch source-granola
Fetching secrets...
No secrets found for connector: 'source-granola'
SECRET_COUNT=0
...
FileNotFoundError: [Errno 2] No such file or directory: '.../source-granola/secrets/config.json'

metadata.yaml has declared SECRET_SOURCE-GRANOLA__CREDS (fileName: config.json, store airbyte-connector-testing-secret-store) since before this PR — but since the connector had no acceptance-test-config.yml, nothing ever consumed it, so my read is that the secret was likely never actually populated. That is consistent with criterion R-1: the read path has never been exercised in CI.

Every other check on this PR is green (34 passed / 13 skipped), including connector lint, connector unit tests, metadata checks, format and docs build. spec and the invalid-config connection case also pass, since neither needs the secret; the connection/discovery/basic_read/fail_read_with_bad_catalog cases are the ones blocked.

Someone with a Granola Enterprise API key and access to the GSM test store needs to populate SECRET_SOURCE-GRANOLA__CREDS with {"api_key": "...", "start_date": "..."} and re-run the checks. I deliberately did not work around this — adding empty_streams, a bypass_reason, or disabling those tests would recreate exactly the blind spot this PR exists to remove. Patrick Nilan (@pnilan), flagging for you as the epic author.


Devin session

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Cross-linking for the consolidation pass triaged by airbyte-support-bot: this PR and #84279 fix the same boundary data loss from two directions (this one tightens the window to PT1S; 84279 removes the windowing altogether, since Granola timestamps carry milliseconds and a PT1S slice still leaves the final sub-second of a boundary day unqueried). #84281 then switches the cursor to updated_at, which the API can only serve unbounded — there is no updated_before parameter.

Full comparison and my suggestion — keep the unbounded cursor plus this PR's acceptance-test-config.yml, which 84279 has no equivalent of — are in #84281 (comment). Not closing or changing anything here; leaving the choice to a human.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

connectors/source/granola hyd-fix Hydra: ai-fix stage has run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants