Skip to content

feat(google-sheets): rich formatting, examples sheet, and readable timestamps#369

Merged
tonyxiao merged 4 commits intomainfrom
sgresh/google-sheets-formatting
May 7, 2026
Merged

feat(google-sheets): rich formatting, examples sheet, and readable timestamps#369
tonyxiao merged 4 commits intomainfrom
sgresh/google-sheets-formatting

Conversation

@sgresh-stripe
Copy link
Copy Markdown

@sgresh-stripe sgresh-stripe commented May 7, 2026

Summary

  • Stripe design-system colors — blurple (#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 banding
  • Semantic column formattinginferSemType() 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 ARRAYFORMULA
  • Overview sheet — branded hero header, live COUNTUNIQUE/COUNTA stream row counts, getting-started guide with merged cells and a clickable Stripe Dashboard hyperlink, edit-warning banner; also removes 'Duplicate Rows" item previously added for debugging purposes
  • Examples sheet — auto-generated pivot tables + embedded charts conditioned on 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); recreated on each setup to prevent chart accumulation; named ranges (stream.field) for formula authoring
  • Readable timestamps — Unix timestamps stored as ISO 8601 strings ("2024-05-14 12:47:00Z") instead of raw integers; human-readable and lexicographically sortable; unixToIso() / isoToUnix() exported from writer.ts; isTimestampField() catches system fields like _updated_at that inferSemType() would otherwise classify as 'system' before reaching the timestamp branch
  • Demo scriptdemo/stripe-to-google-sheets.sh for end-to-end pipeline-setup + pipeline-sync with spreadsheet ID round-trip

Screenshots

image Screenshot 2026-05-07 at 10 33 31 AM Screenshot 2026-05-07 at 10 34 08 AM

Test plan

  • Run demo/stripe-to-google-sheets.sh with valid Stripe + Google credentials and verify the spreadsheet URL is printed
  • Confirm Overview sheet renders with blurple header, stream row counts, and clickable dashboard link
  • Confirm Examples sheet has charts and pivot tables for available streams
  • Confirm data sheet tabs have correct tab colors, header tints, column widths, and Open ↗ companion column in the correct row
  • Confirm timestamp columns (e.g. created, _updated_at) show ISO strings like "2024-05-14 12:47:00Z" instead of raw integers
  • Re-run setup on existing spreadsheet and verify idempotency (no duplicate charts, bandings, or conditional format rules)
  • Run pnpm test (unit tests)

…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
@sgresh-stripe sgresh-stripe changed the base branch from dev to main May 7, 2026 17:40
Copy link
Copy Markdown
Collaborator

@tonyxiao tonyxiao left a comment

Choose a reason for hiding this comment

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

approving for demo, got some follow ups we should address later

@tonyxiao tonyxiao merged commit a14965a into main May 7, 2026
17 of 19 checks passed
Copy link
Copy Markdown
Collaborator

@tonyxiao tonyxiao left a comment

Choose a reason for hiding this comment

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

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})`])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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:

  1. Change these formulas to compare against ISO string boundaries: ">="&TEXT(EDATE(...),"YYYY-MM-DD") (lexicographic comparison works on ISO strings)
  2. Keep timestamp fields as raw numbers in the sheet and rely on Sheets number formatting for display
  3. 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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@tonyxiao tonyxiao deleted the sgresh/google-sheets-formatting branch May 7, 2026 18:13
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.

2 participants