fix(source-granola): stop dropping notes on incremental window boundaries - #84279
fix(source-granola): stop dropping notes on incremental window boundaries#84279devin-ai-integration[bot] wants to merge 3 commits into
Conversation
Co-Authored-By: bot_apk <apk@cognition.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
👋 Greetings, Airbyte Team Member!Here are some helpful tips and reminders for your convenience. 💡 Show Tips and TricksPR Slash CommandsAirbyte Maintainers (that's you!) can execute the following slash commands on your PR:
📚 Show Repo GuidanceHelpful Resources
|
|
Note Autopilot progressive rollouts are not enabled for the following modified connector(s):
This is a courtesy heads-up only — it does not block merge or fail any check. |
Co-Authored-By: bot_apk <apk@cognition.ai>
|
Deploy preview for airbyte-docs ready!
Deployed with vercel-action |
|
Co-Authored-By: bot_apk <apk@cognition.ai>
What
source-granola'snotesstream silently drops records that were created on an incremental slice-boundary date.The
notesDatetimeBasedCursorsliced a 30-day window (step: P30D) with bare-date bounds (datetime_format: "%Y-%m-%d") andcursor_granularity: P1D. Granola interprets a bare date as midnight, socreated_before=Dexcludes everything that happened during dayD, whileP1Dgranularity starts the next slice atD+1. Every note created on a boundary date falls into that gap, and the sync still reports success.The reporter measured 9 of 142 notes lost with
start_date=2025-10-12, all landing exactly on the four 30-day boundaries. Reproduced directly against the API (from the issue):while two notes exist on that date.
Resolves https://github.com/airbytehq/airbyte-internal-issues/issues/16931:
This is the non-breaking half of that issue (I-2). The cursor switch from
created_attoupdated_at(I-1) is a state-format change and ships separately as a breaking release, so this fix can merge on its own.How
The windowing is removed rather than made finer-grained, and slice bounds are formatted as full timestamps:
cursor_datetime_formats: - "%Y-%m-%dT%H:%M:%SZ" - "%Y-%m-%dT%H:%M:%S.%fZ" - datetime_format: "%Y-%m-%d" + - "%Y-%m-%d" + datetime_format: "%Y-%m-%dT%H:%M:%SZ" - end_time_option: {field_name: created_before, ...} - end_datetime: {...} - step: P30D - cursor_granularity: P1DTwo points worth a reviewer's attention:
cursor_granularity: PT1S(the fix sketched in the issue): a windowed variant still leaves a gap of up to one granularity unit betweencreated_before=Tandcreated_after=T+1s, and Granola timestamps carry milliseconds, so a note created inside that second would still be dropped. With noend_time_option/stepthere is no boundary to fall through. The API has noupdated_beforeparameter either, so the follow-upupdated_atcursor has to be unbounded regardless — this lands the connector on that shape now."%Y-%m-%d"is required incursor_datetime_formats. Existing connections have persisted bare-date state ({"created_at": "2026-06-01"}).CustomFormatConcurrentStreamStateConverter.parse_timestampraisesValueErrorfor any state value that matches none ofcursor_datetime_formats + [datetime_format], so without that entry every existing sync would fail on its first read. Keeping it makes the state-format change forward-and-backward compatible, i.e. non-breaking and no reset required.The user-facing
start_datespec field is untouched and still takesYYYY-MM-DD; only the cursor's internal slice/state formatting changed.Trade-off: with a single slice,
notesnow checkpoints once at the end of the stream rather than every 30 days, so an interrupted backfill resumes from the previous state instead of mid-range. Givennotesvolume and that correctness was the failing property here, that is the intended exchange.Declarative-First Evaluation
Entirely declarative — a
DatetimeBasedCursorconfiguration change. Nocomponents.py, no custom Python component.Review guide
airbyte-integrations/connectors/source-granola/manifest.yaml— thenotesincremental_syncblockairbyte-integrations/connectors/source-granola/unit_tests/test_notes_incremental_sync.py— new testsmetadata.yaml/docs/integrations/sources/granola.md— 0.2.12 bump and changelogTest Coverage
New connector-local
unit_tests/project (following thesource-gongmanifest-only layout)./v1/notesis mocked with a callback that emulates the API'screated_after/created_beforesemantics as measured in the issue, and the declarative source is read end to end:test_boundary_date_note_is_not_dropped— a note whosecreated_atlands exactly on a former 30-day boundary. Verified failing against the pre-fix manifest for the right reason (['before-boundary', 'after-boundary'] != ['before-boundary', 'on-boundary', 'after-boundary']) and passing with the fix.test_notes_request_is_unbounded— asserts the exact outgoing query (created_after=2026-01-01T00:00:00Z, nocreated_before, single request).test_legacy_date_state_is_accepted_and_emits_iso_state— pre-fix bare-date state syncs successfully and re-emits state in the new format.poe test-unit-tests: 3 passed.poe test-integration-tests: 2 passed, 6 skipped (no acceptance-test scenarios configured for this connector — unchanged by this PR).No live Granola credentials were available in this environment (
SECRET_SOURCE-GRANOLA__CREDSwas not retrievable), so the API behavior encoded in the mock comes from the reporter'scurloutput and the vendor OpenAPI spec rather than from a sync I ran myself.User Impact
Notes created on an incremental window boundary date are no longer dropped. Existing connections keep syncing with their current state — no reset, no schema change, no config change.
Can this PR be safely reverted and rolled back?
Reverting restores the previous behavior (including the record loss). State written by 0.2.12 is a full timestamp; the pre-0.2.12 cursor accepts that format too, so a rollback does not break state parsing.
Link to Devin session: https://app.devin.ai/sessions/ce7406d5bb334fc5a23acbbc19c0db2d