feat(google-sheets): rich formatting, examples sheet, and readable timestamps#369
feat(google-sheets): rich formatting, examples sheet, and readable timestamps#369
Conversation
…mestamps
Adds comprehensive visual polish to the Google Sheets destination and a
demo script for end-to-end Stripe → Sheets syncs.
**Formatting**
- Sail design-system color palette (blurple headers, semantic tab colors,
Stripe-brand conditional formatting for status enums)
- Per-column semantic type inference (id / timestamp / amount / boolean /
enum / object / system) driving header tints, column widths, and row
banding
- Frozen header + first column on all data sheets
- Human-readable "Sentence case" column headers (snake_case on the wire)
- Dashboard deep-link companion column per stream (ARRAYFORMULA, header
in row 1 so sync writes can't overwrite it)
**Overview sheet**
- Stripe-branded hero header (blurple bg, white bold text)
- Live stream row counts (COUNTUNIQUE / COUNTA formulas)
- Getting-started guide with merged cells and Stripe Dashboard hyperlink
- Warning banner for the no-edit policy
**Examples sheet**
- Auto-generated pivot tables + embedded charts from available streams:
Subscription Status (PIE), New Customers by Month (COLUMN), Payment
Volume by Status (BAR), Products Active vs Archived (PIE), Revenue by
Currency (BAR), Invoice Revenue by Subscription Status (BAR)
- Sheet deleted and recreated on each setup to prevent chart accumulation
- Named ranges (stream.field) for formula authoring
**Timestamp display**
- Unix timestamps stored as ISO 8601 strings ("2024-05-14 12:47:00Z")
instead of raw integers — human-readable, lexicographically sortable,
losslessly reversible via isoToUnix()
- isTimestampField() exported from writer.ts handles system fields like
_updated_at (field.endsWith('_at')) that inferSemType() classifies as
'system' before reaching the timestamp branch
- Stale-write / in-batch dedup comparisons unchanged: ISO string ordering
is identical to unix ordering for the same UTC timezone
**Demo script**
- stripe-to-google-sheets.sh: end-to-end pipeline-setup + pipeline-sync
with spreadsheet ID round-trip and sheet URL printed to stderr
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Committed-By-Agent: claude
tonyxiao
left a comment
There was a problem hiding this comment.
approving for demo, got some follow ups we should address later
tonyxiao
left a comment
There was a problem hiding this comment.
Overall this is a well-crafted feature that significantly improves the Sheets UX. One correctness bug (timestamps vs formulas) that I'd fix before merging, and a latent issue with displayToField lossiness worth noting.
| // Unix timestamp boundaries for each month | ||
| const startUnix = `(EDATE(DATE(YEAR(TODAY()),MONTH(TODAY()),1),${-i})-DATE(1970,1,1))*86400` | ||
| const endUnix = `(EDATE(DATE(YEAR(TODAY()),MONTH(TODAY()),1),${-(i - 1)})-DATE(1970,1,1))*86400` | ||
| rows.push([monthLabel, `=COUNTIFS(${createdRange},">="&${startUnix},${createdRange},"<"&${endUnix})`]) |
There was a problem hiding this comment.
Bug: timestamp format mismatch breaks this formula.
The created column is now stored as ISO strings (e.g. "2024-05-14 12:47:00Z") via unixToIso(), but this COUNTIFS formula compares against numeric Unix timestamp boundaries ((EDATE(...)-DATE(1970,1,1))*86400). Google Sheets can't meaningfully compare a text cell against a numeric threshold — this chart will always show zeros.
Fix options:
- Change these formulas to compare against ISO string boundaries:
">="&TEXT(EDATE(...),"YYYY-MM-DD")(lexicographic comparison works on ISO strings) - Keep timestamp fields as raw numbers in the sheet and rely on Sheets number formatting for display
- Use a helper formula that parses the ISO string back to a serial date before comparing
| } | ||
|
|
||
| /** Inverse of {@link fieldToDisplay}. Idempotent — safe to apply to already-field-format strings. */ | ||
| export function displayToField(display: string): string { |
There was a problem hiding this comment.
Latent bug: lossy roundtrip for multi-word fields with uppercase.
fieldToDisplay("API_version") → "API version" → displayToField("API version") → "api_version" (not the original).
This matters because displayToField is called on headers read back from the sheet (index.ts:408) during incremental syncs. Any field with uppercase beyond the first character will silently map to the wrong key. In practice Stripe's schema is all-lowercase snake_case so this may not fire today, but it's fragile if custom object fields or future API fields use uppercase.
Summary
#533AFD) headers on Overview/Examples, semantic tab colors per stream category (blue for customers/invoices, green for subscriptions, orange for payments, red for refunds/disputes), conditional formatting for status enum values (green/red/amber), and alternating row bandinginferSemType()classifies each field (id / timestamp / amount / boolean / enum / object / system) to drive per-column header tints, widths, and a Stripe Dashboard deep-link companion column (Open ↗) via ARRAYFORMULAstream.field) for formula authoring"2024-05-14 12:47:00Z") instead of raw integers; human-readable and lexicographically sortable;unixToIso()/isoToUnix()exported fromwriter.ts;isTimestampField()catches system fields like_updated_atthatinferSemType()would otherwise classify as'system'before reaching the timestamp branchdemo/stripe-to-google-sheets.shfor end-to-end pipeline-setup + pipeline-sync with spreadsheet ID round-tripScreenshots
Test plan
demo/stripe-to-google-sheets.shwith valid Stripe + Google credentials and verify the spreadsheet URL is printedOpen ↗companion column in the correct rowcreated,_updated_at) show ISO strings like"2024-05-14 12:47:00Z"instead of raw integerspnpm test(unit tests)