Queries · q331 · Data Protection Officer · week 2, night 3
“When a shopper withdraws marketing_email consent, do we actually stop sending them marketing campaign emails, or does sending continue after withdrawal?”
Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told
Week 2; shared with Henrik Aalto, Chief Information Security Officer
A data protection authority asked how we handle privacy: how many privacy requests we receive and how fast we complete them, how consent is recorded, and what remains after an account is deleted.
The manager, at the start of week 2
Subject: Week 2 — DPA inquiry changes our priorities
Priyanka,
Good work — this is exactly the kind of digging I wanted. A few things surprised me and now matter more because of the regulator inquiry (which I'm handling jointly with Henrik Aalto, CISO — our response needs to be accurate and defensible, not just reassuring).
What struck me:
So, for this week, please prioritize work that lets us answer the DPA's three questions honestly: volume and speed of privacy requests, how consent is captured and whether it's reliable, and what data persists post-deletion. That's the deliverable Henrik and I need to brief the board audit committee before we respond.
My questions:
If all four can't fit, start with 2 and 4 — those go directly into the regulator response.
Thanks, Ines
Subject: Security angle for the DPA response
Hi Priyanka,
A few things from my side that would strengthen the privacy response:
Happy to compare notes before you send anything to data engineering.
Henrik
The analyst · week 2, night 3, request 3 of 3
Two good resolutions from last night: the data_exports status audit shows all 11 exports have completed_at AND file_url populated — they did fulfill, they're just labeled 'expired' because the download link's retention window has since passed, so I'm closing out the "did access requests actually deliver data" concern. The pre-2023 DSAR rows show created_at exactly equal to received_at for all 12 — that's the opposite of what I'd expect from a deliberate migration backfill (where insertion would postdate the original event), so I can't confidently call this "verified legitimate backfill"; I'm flagging it as an open inconsistency with the documented 2023-01-01 history start that needs an answer from engineering, not more SQL from me. The legal-hold evidence (report 3) is useful but indirect: 2 of 3 accounts already show a partial refund 6–12 months ago with zero ticket activity since, suggesting the disputes may already be effectively resolved while the hold was never released — worth flagging to Ines as "likely stale, recommend review," with the caveat that I matched by "most recent payment for that user," not a guaranteed link to the specific disputed payment. With priorities 1, 2, and 4 now substantially covered, tonight I'm moving to the untouched items: priority 3 (consent reliability — is the analytics_cookies anomaly isolated or systemic?) and Henrik's two unaddressed security asks (what persists in sessions/logins/access_log after erasure; do we actually stop marketing to someone who withdrew consent). Henrik's third ask (closure time for security-data requests) still has no clean way to tag which DSARs touch security data, so I'm deferring it to the final night or flagging it as not currently answerable.
The engineer · 1 dry run on the empty schema, then the submission
SELECT COUNT(DISTINCT w.user_key) AS withdrawn_users, COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) AS contacted_after_withdrawal_users, ROUND(100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0), 2) AS pct_contacted_after_withdrawal, COUNT(cm.id) AS total_post_withdrawal_sends, MAX(COALESCE(cm.sent_at, cm.created_at)::date - w.withdrawn_at::date) AS max_days_withdrawal_to_send, MAX(COALESCE(cm.sent_at, cm.created_at)) AS most_recent_post_withdrawal_send FROM ( SELECT tenant_id, user_id, occurred_at AS withdrawn_at, ROW_NUMBER() OVER (ORDER BY tenant_id, user_id) AS user_key FROM ( SELECT tenant_id, user_id, status, occurred_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC, id DESC) AS rn FROM consent_records WHERE purpose = 'marketing_email' ) ranked WHERE rn = 1 AND status = 'withdrawn' ) w LEFT JOIN campaign_messages cm ON cm.tenant_id = w.tenant_id AND cm.user_id = w.user_id AND COALESCE(cm.sent_at, cm.created_at) > w.withdrawn_at
SELECT COUNT(DISTINCT w.user_key) AS withdrawn_users, COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) AS contacted_after_withdrawal_users, ROUND(100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0), 2) AS pct_contacted_after_withdrawal, COUNT(cm.id) AS total_post_withdrawal_sends, MAX(COALESCE(cm.sent_at, cm.created_at)::date - w.withdrawn_at::date) AS max_days_withdrawal_to_send, MAX(COALESCE(cm.sent_at, cm.created_at)) AS most_recent_post_withdrawal_sendFROM ( SELECT tenant_id, user_id, occurred_at AS withdrawn_at, ROW_NUMBER() OVER (ORDER BY tenant_id, user_id) AS user_key FROM ( SELECT tenant_id, user_id, status, occurred_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC, id DESC) AS rn FROM consent_records WHERE purpose = 'marketing_email' ) ranked WHERE rn = 1 AND status = 'withdrawn') wLEFT JOIN campaign_messages cm ON cm.tenant_id = w.tenant_id AND cm.user_id = w.user_id AND COALESCE(cm.sent_at, cm.created_at) > w.withdrawn_atORDER BY withdrawn_users
“Submitted. The report returns one row with: total users whose latest marketing_email consent is 'withdrawn', how many of those received a campaign email after withdrawal, the contact rate, total post-withdrawal sends, the worst-case lag in days, and the most recent offending send date — exactly the grain and columns Priyanka asked for.”
As Priyanka Desai saw it, from the small dataset the agents worked on
withdrawn_users | contacted_after_withdrawal_users | pct_contacted_after_withdrawal | total_post_withdrawal_sends | max_days_withdrawal_to_send | most_recent_post_withdrawal_send
----------------+----------------------------------+--------------------------------+-----------------------------+-----------------------------+---------------------------------
431 | 0 | 0.0 | 0 | NULL | NULL
(1 rows)The memo and the queries that followed; links found by title, so inferred
It builds on:
Changed only where the benchmark needs the same result on every system
SELECT COUNT(DISTINCT w.user_key) AS withdrawn_users, COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) AS contacted_after_withdrawal_users, ROUND(100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0), 2) AS pct_contacted_after_withdrawal, 100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0) AS pct_contacted_after_withdrawal, COUNT(cm.id) AS total_post_withdrawal_sends, MAX(COALESCE(cm.sent_at, cm.created_at)::date - w.withdrawn_at::date) AS max_days_withdrawal_to_send, MAX({{date_diff_days(w.withdrawn_at, COALESCE(cm.sent_at, cm.created_at))}}) AS max_days_withdrawal_to_send, MAX(COALESCE(cm.sent_at, cm.created_at)) AS most_recent_post_withdrawal_sendFROM ( SELECT tenant_id, user_id, occurred_at AS withdrawn_at, ROW_NUMBER() OVER (ORDER BY tenant_id, user_id) AS user_key ROW_NUMBER() OVER (ORDER BY tenant_id NULLS LAST, user_id NULLS LAST) AS user_key FROM ( SELECT tenant_id, user_id, status, occurred_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC, id DESC) AS rn ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC NULLS FIRST, id DESC NULLS FIRST) AS rn FROM consent_records WHERE purpose = 'marketing_email' ) ranked WHERE rn = 1 AND status = 'withdrawn') wLEFT JOIN campaign_messages cm ON cm.tenant_id = w.tenant_id AND cm.user_id = w.user_id AND COALESCE(cm.sent_at, cm.created_at) > w.withdrawn_atORDER BY withdrawn_usersORDER BY withdrawn_users NULLS LAST
SELECT COUNT(DISTINCT w.user_key) AS withdrawn_users, COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) AS contacted_after_withdrawal_users, 100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0) AS pct_contacted_after_withdrawal, COUNT(cm.id) AS total_post_withdrawal_sends, MAX(DATE_DIFF('day', w.withdrawn_at, COALESCE(cm.sent_at, cm.created_at))) AS max_days_withdrawal_to_send, MAX(COALESCE(cm.sent_at, cm.created_at)) AS most_recent_post_withdrawal_send FROM ( SELECT tenant_id, user_id, occurred_at AS withdrawn_at, ROW_NUMBER() OVER (ORDER BY tenant_id NULLS LAST, user_id NULLS LAST) AS user_key FROM ( SELECT tenant_id, user_id, status, occurred_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC NULLS FIRST, id DESC NULLS FIRST) AS rn FROM consent_records WHERE purpose = 'marketing_email' ) ranked WHERE rn = 1 AND status = 'withdrawn' ) w LEFT JOIN campaign_messages cm ON cm.tenant_id = w.tenant_id AND cm.user_id = w.user_id AND COALESCE(cm.sent_at, cm.created_at) > w.withdrawn_at ORDER BY withdrawn_users NULLS LAST
SELECT COUNT(DISTINCT w.user_key) AS withdrawn_users, COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) AS contacted_after_withdrawal_users, 100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0) AS pct_contacted_after_withdrawal, COUNT(cm.id) AS total_post_withdrawal_sends, MAX((CAST(COALESCE(cm.sent_at, cm.created_at) AS DATE) - CAST(w.withdrawn_at AS DATE))) AS max_days_withdrawal_to_send, MAX(COALESCE(cm.sent_at, cm.created_at)) AS most_recent_post_withdrawal_send FROM ( SELECT tenant_id, user_id, occurred_at AS withdrawn_at, ROW_NUMBER() OVER (ORDER BY tenant_id NULLS LAST, user_id NULLS LAST) AS user_key FROM ( SELECT tenant_id, user_id, status, occurred_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC NULLS FIRST, id DESC NULLS FIRST) AS rn FROM consent_records WHERE purpose = 'marketing_email' ) ranked WHERE rn = 1 AND status = 'withdrawn' ) w LEFT JOIN campaign_messages cm ON cm.tenant_id = w.tenant_id AND cm.user_id = w.user_id AND COALESCE(cm.sent_at, cm.created_at) > w.withdrawn_at ORDER BY withdrawn_users NULLS LAST
SELECT COUNT(DISTINCT w.user_key) AS withdrawn_users, COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) AS contacted_after_withdrawal_users, 100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0) AS pct_contacted_after_withdrawal, COUNT(cm.id) AS total_post_withdrawal_sends, MAX(dateDiff('day', w.withdrawn_at, COALESCE(cm.sent_at, cm.created_at))) AS max_days_withdrawal_to_send, MAX(COALESCE(cm.sent_at, cm.created_at)) AS most_recent_post_withdrawal_send FROM ( SELECT tenant_id, user_id, occurred_at AS withdrawn_at, ROW_NUMBER() OVER (ORDER BY tenant_id NULLS LAST, user_id NULLS LAST) AS user_key FROM ( SELECT tenant_id, user_id, status, occurred_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC NULLS FIRST, id DESC NULLS FIRST) AS rn FROM consent_records WHERE purpose = 'marketing_email' ) ranked WHERE rn = 1 AND status = 'withdrawn' ) w LEFT JOIN campaign_messages cm ON cm.tenant_id = w.tenant_id AND cm.user_id = w.user_id AND COALESCE(cm.sent_at, cm.created_at) > w.withdrawn_at ORDER BY withdrawn_users NULLS LAST
The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted
Report request from Priyanka Desai (analyst for the Data Protection Officer): Title: Post-withdrawal marketing suppression check Question: When a shopper withdraws marketing_email consent, do we actually stop sending them marketing campaign emails, or does sending continue after withdrawal? Grain: single summary row across all users with an effective withdrawn marketing_email consent as of 2026-01-05 (i.e. their most recent consent_records row for purpose='marketing_email' has status='withdrawn', with no later 'granted' row) Columns: - users with effective withdrawal as of today (count) - of those, users who received at least 1 campaign_messages send after their withdrawal date (count) - pct of withdrawn users contacted after withdrawal - total post-withdrawal sends across all such users (count) - max days between withdrawal and a post-withdrawal send (worst case observed) - most recent post-withdrawal send date observed Details: Source: consent_records (window function per user to find the latest marketing_email row, filtered to status='withdrawn') joined to campaign_messages on user_id, counting sends where campaign_messages.sent_at (or created_at if sent_at is null) is after the withdrawal's occurred_at. Platform-wide, full history. This is a single aggregate row by design — the underlying population of withdrawn users is in the thousands (per last week's consent trend), so a per-user listing would blow past the row cap; we need the magnitude and worst case, not a row-by-row listing. If the 'contacted after withdrawal' count is non-trivial, that's evidence we don't suppress sends promptly. Check your query with the warehouse's dry_run tool, then hand it in with submit.
dry_runSELECT COUNT(DISTINCT w.user_key) AS withdrawn_users, COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) AS contacted_after_withdrawal_users, ROUND(100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0), 2) AS pct_contacted_after_withdrawal, COUNT(cm.id) AS total_post_withdrawal_sends, MAX(COALESCE(cm.sent_at, cm.created_at)::date - w.withdrawn_at::date) AS max_days_withdrawal_to_send, MAX(COALESCE(cm.sent_at, cm.created_at)) AS most_recent_post_withdrawal_send FROM ( SELECT tenant_id, user_id, occurred_at AS withdrawn_at, ROW_NUMBER() OVER (ORDER BY tenant_id, user_id) AS user_key FROM ( SELECT tenant_id, user_id, status, occurred_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC, id DESC) AS rn FROM consent_records WHERE purpose = 'marketing_email' ) ranked WHERE rn = 1 AND status = 'withdrawn' ) w LEFT JOIN campaign_messages cm ON cm.tenant_id = w.tenant_id AND cm.user_id = w.user_id AND COALESCE(cm.sent_at, cm.created_at) > w.withdrawn_at
STATUS: COMPILES (dry run on the empty schema, no data read) COLUMNS: withdrawn_users, contacted_after_withdrawal_users, pct_contacted_after_withdrawal, total_post_withdrawal_sends, max_days_withdrawal_to_send, most_recent_post_withdrawal_send
submitSELECT COUNT(DISTINCT w.user_key) AS withdrawn_users, COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) AS contacted_after_withdrawal_users, ROUND(100.0 * COUNT(DISTINCT CASE WHEN cm.id IS NOT NULL THEN w.user_key END) / NULLIF(COUNT(DISTINCT w.user_key), 0), 2) AS pct_contacted_after_withdrawal, COUNT(cm.id) AS total_post_withdrawal_sends, MAX(COALESCE(cm.sent_at, cm.created_at)::date - w.withdrawn_at::date) AS max_days_withdrawal_to_send, MAX(COALESCE(cm.sent_at, cm.created_at)) AS most_recent_post_withdrawal_send FROM ( SELECT tenant_id, user_id, occurred_at AS withdrawn_at, ROW_NUMBER() OVER (ORDER BY tenant_id, user_id) AS user_key FROM ( SELECT tenant_id, user_id, status, occurred_at, ROW_NUMBER() OVER (PARTITION BY tenant_id, user_id ORDER BY occurred_at DESC, id DESC) AS rn FROM consent_records WHERE purpose = 'marketing_email' ) ranked WHERE rn = 1 AND status = 'withdrawn' ) w LEFT JOIN campaign_messages cm ON cm.tenant_id = w.tenant_id AND cm.user_id = w.user_id AND COALESCE(cm.sent_at, cm.created_at) > w.withdrawn_at ORDER BY withdrawn_users
Submitted to tonight's batch. Your work on this request is done.