feat(trigger): add Google Sheets, Drive, and Calendar polling triggers#4081
feat(trigger): add Google Sheets, Drive, and Calendar polling triggers#4081waleedlatif1 wants to merge 17 commits intostagingfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Wires these triggers into the corresponding Google blocks (spreading trigger subBlocks and enabling Introduces a new subBlock mode Reviewed by Cursor Bugbot for commit ef82ce6. Configure here. |
Greptile SummaryThis PR adds three new polling triggers — Google Sheets (new row detection), Google Drive (file change tracking via cursor), and Google Calendar (event changes via
Confidence Score: 3/5Not safe to merge as-is — the Drive cursor stall bug causes silent, permanent data loss in a plausible high-traffic scenario One confirmed P1 logic bug in the Drive polling handler: when the first page returns exactly maxFiles (50) changes with more pending, lastNextPageToken is never captured and the cursor reverts to the old position on every poll. This silently prevents all subsequent Drive changes from being detected. The Calendar and Sheets handlers look correct after prior-round fixes, and the idempotency retryFailures enhancement is solid. The Drive cursor fix is small but is a blocking correctness issue. apps/sim/lib/webhooks/polling/google-drive.ts — the fetchChanges cursor resolution logic (lines 260–278) Important Files Changed
Sequence DiagramsequenceDiagram
participant Cron as Cron Job (1 min)
participant Poll as Poll API Route
participant Handler as Polling Handler
participant Google as Google API
participant Idem as Idempotency Service
participant Proc as Webhook Processor
Cron->>Poll: POST /api/webhooks/poll/{provider}
Poll->>Handler: pollWebhook(ctx)
alt First Poll
Handler->>Google: Seed cursor / timestamp
Handler-->>Poll: success (emit nothing)
else Subsequent Polls
Handler->>Google: Fetch changes since last cursor
Google-->>Handler: changes[]
loop For each change
Handler->>Idem: executeWithIdempotency(key, op)
alt Cache hit (success)
Idem-->>Handler: stored result (no re-fire)
else Failed key + retryFailures=true
Idem->>Idem: deleteKey() then retry
Idem->>Proc: processPolledWebhookEvent()
Proc-->>Idem: result
Idem-->>Handler: result
else New key
Idem->>Proc: processPolledWebhookEvent()
Proc-->>Idem: result
Idem-->>Handler: result
end
end
alt All succeed
Handler->>Handler: Advance cursor/timestamp
else Any failure
Handler->>Handler: Revert cursor/timestamp
end
Handler-->>Poll: success/failure
end
Reviews (9): Last reviewed commit: "fix(polling): remove extraneous comment ..." | Re-trigger Greptile |
|
@greptile |
|
@cursor review |
|
@greptile |
|
@cursor review |
|
@greptile |
|
@cursor review |
3d6edeb to
e14ff04
Compare
|
@greptile |
|
@cursor review |
|
@greptile |
- Fix Drive cursor stall: use nextPageToken as resume point when breaking early from pagination instead of re-using the original token - Eliminate redundant Drive API call in Sheets poller by returning modifiedTime from the pre-check function - Add 403/429 rate-limit handling to Sheets API calls matching the Calendar handler pattern - Remove unused changeType field from DriveChangeEntry interface - Rename triggers/google_drive to triggers/google-drive for consistency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
isDriveFileUnchanged short-circuited when lastModifiedTime was undefined, never calling the Drive API — so currentModifiedTime was never populated, creating a permanent chicken-and-egg loop. Now always calls the Drive API and returns the modifiedTime regardless of whether there's a previous value to compare against. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix fetchHeaderRow to throw on 403/429 rate limits instead of silently returning empty headers (prevents rows from being processed without headers and lastKnownRowCount from advancing past them permanently) - Fix Drive pagination to avoid advancing resume cursor past sliced changes (prevents permanent change loss when allChanges > maxFiles) - Remove unused logger import from Google Drive trigger config
…empotency key - Sheets: only advance lastKnownRowCount by processedCount when there are failures, so failed rows are retried on the next poll cycle (idempotency deduplicates already-processed rows on re-fetch) - Drive: add fallback for change.time in idempotency key to prevent key collisions if the field is ever absent from the API response
… Drive API failure - Remove unused `now` variable from Google Drive polling handler - Preserve stored lastModifiedTime when Drive API pre-check fails (previously wrote undefined, disabling the optimization until the next successful Drive API call)
… calendar, drive handlers
…ot, fix calendar inclusive updatedMin
- Add 'trigger-advanced' mode to SubBlockConfig so canonical pairs work in trigger mode - Fix buildCanonicalIndex: trigger-mode subblocks don't overwrite non-trigger basicId, deduplicate advancedIds from block spreads - Update editor, subblock layout, and trigger config aggregation to include trigger-advanced subblocks - Replace dropdown+fetchOptions in Calendar/Sheets/Drive pollers with file-selector (basic) + short-input (advanced) canonical pairs - Add canonicalParamId: 'oauthCredential' to triggerCredentials for selector context resolution - Update polling handlers to read canonical fallbacks (calendarId||manualCalendarId, etc.)
47c7b3e to
2180127
Compare
|
@greptile |
|
@cursor review |
|
@greptile |
|
@cursor review |
| const overLimit = allChanges.length >= maxFiles | ||
|
|
||
| if (!hasMore || overLimit || pages >= MAX_PAGES) { | ||
| if (hasMore && !overLimit) { | ||
| lastNextPageToken = data.nextPageToken as string | ||
| } | ||
| break | ||
| } | ||
|
|
||
| lastNextPageToken = data.nextPageToken as string | ||
| currentPageToken = data.nextPageToken as string | ||
| } | ||
|
|
||
| const slicingOccurs = allChanges.length > maxFiles | ||
| const resumeToken = slicingOccurs | ||
| ? (lastNextPageToken ?? config.pageToken!) | ||
| : (newStartPageToken ?? lastNextPageToken ?? config.pageToken!) | ||
|
|
||
| return { changes: allChanges.slice(0, maxFiles), newStartPageToken: resumeToken } |
There was a problem hiding this comment.
Drive cursor stalls permanently when the first page is exactly full
When pageSize === maxFiles (default both = 50) and the API returns exactly 50 changes with hasMore = true, the inner guard if (hasMore && !overLimit) is false (overLimit wins), so lastNextPageToken is never assigned in the break block. After the loop: slicingOccurs = (50 > 50) = false, the resolution chain tries newStartPageToken ?? lastNextPageToken ?? old_cursor. Since this is not the final page, newStartPageToken is absent and lastNextPageToken is still undefined, so the old cursor is reused.
Every subsequent poll re-fetches those same 50 changes as idempotency cache hits, marks success, and writes back the old cursor. Changes beyond the first 50 are permanently invisible until the backlog drops below 50 organically.
The previous fix for the "overshoots" case (addressed via slicingOccurs) covers length > maxFiles but not length === maxFiles. The guard should capture data.nextPageToken as lastNextPageToken whenever hasMore is true at the break point — not just when !overLimit.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ef82ce6. Configure here.
| const computedEventType = determineEventType(event) | ||
| if (eventTypeFilter && computedEventType !== eventTypeFilter) { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Calendar advances cursor past events on unprocessed pages
Medium Severity
latestUpdated is tracked for ALL events in the batch — including those skipped by eventTypeFilter — before the filter check. When the event with the highest updated timestamp is a filtered-out event, the cursor advances past it even if that timestamp is beyond the pagination boundary. If maxEvents caused truncation via allEvents.slice(0, maxEvents), unprocessed events from later pages sharing timestamps near the filtered event's updated could be skipped permanently.
Reviewed by Cursor Bugbot for commit ef82ce6. Configure here.


Summary
changes.listAPI with opaque cursor (no clock skew, catches deletes)updatedMin+singleEvents=true(expanded recurring events)modifiedTimepre-check (saves Sheets quota)generate-docs.tsdiscovers themintegrations.jsonwith new trigger entriesadd-triggerskill with polling instructions and versioned block wiring guidancetrigger-advancedmode for subblocks for advanced mode fields in trigger modeType of Change
Testing
Tested manually — type-check passes, lint passes,
integrations.jsonregenerated correctlyChecklist