Person Touches
The person_touches table/view tracks person touches in form responses, pings, back-imported paid-media leads (EDDY), GA4 (where possible), etc.
Create person_touches
Section titled “Create person_touches”CREATE OR REPLACE TABLE person_touches AS
SELECT pi.person_id, pi.created_at AS touched_at, 'ping' AS touch_type, 'engagement' AS touch_category, pi.url, split_part(regexp_replace(lower(trim(pi.utm_campaign)), '_+$', ''), ',', 1) AS utm_campaign, split_part(regexp_replace(lower(trim(pi.utm_source)), '_+$', ''), ',', 1) AS utm_source, split_part(regexp_replace(lower(trim(pi.utm_medium)), '_+$', ''), ',', 1) AS utm_medium, NULL AS utm_content, NULL AS utm_term, 'pings' AS sourceFROM pings piJOIN persons p ON pi.person_id = p.idWHERE pi.person_id IS NOT NULL
UNION ALL
SELECT fr.record_id AS person_id, fr.submitted_at AS touched_at, 'form_response' AS touch_type, 'engagement' AS touch_category, fr.url, split_part(regexp_replace(lower(trim(fr.utm_campaign)), '_+$', ''), ',', 1) AS utm_campaign, split_part(regexp_replace(lower(trim(fr.utm_source)), '_+$', ''), ',', 1) AS utm_source, split_part(regexp_replace(lower(trim(fr.utm_medium)), '_+$', ''), ',', 1) AS utm_medium, NULL AS utm_content, NULL AS utm_term, 'form_responses' AS sourceFROM form_responses frJOIN persons p ON fr.record_id = p.idWHERE fr.record_id IS NOT NULL
UNION ALL
-- EDDY paid media leads: UTM data is back-imported into person-level Slate-- fields rather than pings or form responses. Each UTM component is a-- separate row in `fields`, so we pivot per person, then scrub as usual.SELECT pu.record_id AS person_id, p.created_at AS touched_at, 'person_utm' AS touch_type, 'exposure' AS touch_category, NULL AS url, split_part(regexp_replace(lower(trim(pu.utm_campaign)), '_+$', ''), ',', 1) AS utm_campaign, split_part(regexp_replace(lower(trim(pu.utm_source)), '_+$', ''), ',', 1) AS utm_source, split_part(regexp_replace(lower(trim(pu.utm_medium)), '_+$', ''), ',', 1) AS utm_medium, split_part(regexp_replace(lower(trim(pu.utm_content)), '_+$', ''), ',', 1) AS utm_content, split_part(regexp_replace(lower(trim(pu.utm_term)), '_+$', ''), ',', 1) AS utm_term, 'person_fields' AS sourceFROM ( SELECT f.record_id, MAX(CASE WHEN f.field = 'person_utm_source' THEN COALESCE(lp.prompt_value, f.value) END) AS utm_source, MAX(CASE WHEN f.field = 'person_utm_medium' THEN COALESCE(lp.prompt_value, f.value) END) AS utm_medium, MAX(CASE WHEN f.field = 'person_utm_campaign' THEN COALESCE(lp.prompt_value, f.value) END) AS utm_campaign, MAX(CASE WHEN f.field = 'person_utm_content' THEN COALESCE(lp.prompt_value, f.value) END) AS utm_content, MAX(CASE WHEN f.field = 'person_utm_term' THEN COALESCE(lp.prompt_value, f.value) END) AS utm_term FROM fields f LEFT JOIN lookup_prompts lp ON f.prompt = lp.id WHERE f.field IN ( 'person_utm_source', 'person_utm_medium', 'person_utm_campaign', 'person_utm_content', 'person_utm_term' ) GROUP BY f.record_id) puJOIN persons p ON pu.record_id = p.idWHERE pu.utm_source IS NOT NULL;Schema
Section titled “Schema”If you’re doing a pro-level import, here’s the final schema. Basic and advanced imports will have fewer columns.
| Column | Type |
|---|---|
| person_id | uuid |
| touch_type | varchar |
| touch_category | varchar |
| url | varchar |
| utm_campaign | varchar |
| utm_source | varchar |
| utm_medium | varchar |
| utm_content | varchar |
| utm_term | varchar |
| source | varchar |
| touched_at | timestamp |
Column values
Section titled “Column values”| Column | Values produced by this query |
|---|---|
touch_type | ping, form_response, person_utm |
touch_category | engagement (pings, form responses), exposure (EDDY person_utm) |
source | pings, form_responses, person_fields |