Experimenting with Snowplow Signals on this very website

●6 MIN READ

Experimenting with Snowplow Signals on this very website

This blog now runs a Snowplow web tracker. Every page view, every ten seconds of reading, every section you scroll past and every click on the menu, the bell or a social link flows through a Snowplow pipeline into Signals, which turns that stream into a handful of attributes per session and serves them back over an API in a few milliseconds.

This is what Signals knows about your visit right now.

What is being computed

Two attribute groups, because some things belong to you and some to this one visit. Both are keyed on anonymous cookies the tracker sets: domain_userid follows you across visits, domain_sessionid resets after Snowplow’s 30-minute session timeout. One request returns both.

Attribute Kept Source events Aggregation
session_source this visit page_view last of UTM source, else Snowplow’s referrer name, else host; internal clicks skipped
sections_read across visits section_view counter
current_section across visits section_view last title
ui_opened across visits ui_click unique list of target
pages_viewed across visits page_view unique list of page_urlpath
page_pings across visits page_ping counter
persona_counts across visits page_view category count of the page entity’s persona

section_view and ui_click are two tiny custom schemas of mine. The section one fires once per heading when it scrolls into the top part of the viewport, the click one fires for the nav toggle, the bell in the corner, and any GitHub, LinkedIn, Instagram, email or RSS link. The attribute group is defined in Snowplow Console.

The widget above asks a Cloudflare Worker for your attributes: every second right after a page loads, every five seconds after that. The Worker holds the API key, the browser never sees it.

What it changes on the website

Those attributes don’t just sit in the widget. The site reads them and changes for you. All of it needs your consent: without it your events are anonymous and Signals can’t tell you apart from anyone else.

The signature in the top bar takes the colours of where you came from. Arrive from Google, LinkedIn, Instagram or TikTok and watch it change.

The bell

The text behind the bell in the corner changes too. Come from social media and it’s looser. Come from LinkedIn and it’s more serious and to the point.

The highlighted posts

Every post is tagged as professional or personal. Read the technical ones and the highlighted posts on the front page fill up with more professional posts. Come from social media or read the personal ones and you get personal posts.

The first one is what a professional reader sees: Snowplow Signals (of course), a Belgian bank account in App Store Connect, and how Prefect and a beefy Hetzner auction box orchestrate my price tracking.

Highlighted posts for a professional reader: Snowplow Signals, Prefect and a Belgian bank account in App Store Connect

The second one is the personal version: my polaroids, chess, and the places I added to Google Street View.

Highlighted posts for a personal reader: Odyssey polaroids, Google Street View and The Queen’s Gambit

The About page

The About page follows the same split. From LinkedIn it’s professional: my skills and what I do. If you rate as more of a personal connection, it’s about my travels and my hobbies.

Tagging every post with a persona

The next step is a site that adapts to who is reading. Mapping URLs to “professional” or “personal” in Signals would get messy with every new post, so the label lives with the content instead. Every post has a persona in its front matter:

title: "How to make an AliExpress affiliate API call"
tags: ["code"]
persona: "pro"

The layout turns that into a small page entity that rides along with every event on the page:

{
  "schema": "iglu:me.vandevliet/page/jsonschema/1-0-0",
  "data": {
    "persona": "pro",
    "tags": ["code"],
    "kind": "post"
  }
}

Signals then needs one attribute: a category count of persona across page views. Read three code posts and one polaroid post and you get {"pro": 3, "personal": 1}.

The catch: Console couldn’t see the entity

Building that attribute in Snowplow Console didn’t work. The property picker kept saying no entities linked to your event, even with the schema in production, attached to the source app and published in a tracking plan with an event specification.

The reason: Console fills that picker from the data catalog, and the catalog is built from what lands in your warehouse. This site has no warehouse, only a stream. The entity was flowing into Signals fine; the UI had simply never seen it.

Signals itself doesn’t care. So the attribute went in through the API instead, and Console shows it like any other one afterwards. In Python, with the Signals SDK, the whole thing is this:

from snowplow_signals import (
    Attribute, EntityProperty, Event, Service, Signals, StreamAttributeGroup,
)

sp = Signals(api_url=API_URL, api_key=API_KEY, api_key_id=API_KEY_ID, org_id=ORG_ID)

persona_counts = Attribute(
    name="persona_counts",
    type="dict",
    events=[Event(vendor="com.snowplowanalytics.snowplow", name="page_view", version="1-0-0")],
    aggregation="category_count",
    property=EntityProperty(vendor="me.vandevliet", name="page", major_version=1, path="persona"),
)

# the group built in Console, plus the one attribute the UI can't build
latest = sp.get_attribute_group(name="web_session").model_dump()
group = StreamAttributeGroup(
    name="web_session",
    version=latest["version"] + 1,          # published versions are immutable
    attribute_key=latest["attribute_key"],
    owner=latest["owner"],
    attributes=[*latest["attributes"], persona_counts],
)

# point the service the widget reads at the new version
service = Service(name="web_demo", owner=latest["owner"], attribute_groups=[group])

sp.publish([group, service])

Fetch the group you made in Console, add the one attribute the UI can’t build, bump the version, point the service at it, publish. Done.

Try it: arrive from somewhere else

The first attribute is where you came from. Signals keeps the last outside source of your visit and ignores clicks within this site. A UTM tag on the link wins; without one it falls back to the referrer, which the Snowplow pipeline turns into a clean name like Google or Instagram.

Click one of these, then watch the widget above and the signature in the top bar change colour. The real redirects go through the platform itself and open in a new tab; the UTM links fake it right here.

Platform Real redirect UTM source link What Signals records
Google google.com/url ?utm_source=google Google via redirect, google via UTM
LinkedIn linkedin.com/safety/go ?utm_source=linkedin LinkedIn via redirect, linkedin via UTM
Instagram l.instagram.com ?utm_source=instagram Instagram via redirect, instagram via UTM
TikTok none, TikTok sends no referrer ?utm_source=tiktok tiktok via UTM

The UTM links fake the visit with a tag. You don’t need one in real life, though. Click my link in an Instagram bio, a LinkedIn post or a Google result and the referrer alone does the job. TikTok is the exception: its in-app browser sends no referrer at all, so only a UTM tag tells Signals you came from there.

Why bother on a personal blog

Well, why not? In my professional life I work day in, day out with Snowplow, Snowflake and data modelling. These days the tools we use change at such a speed that you have to keep up by actually trying things. So when I saw I could open a trial account on Snowplow and play with Signals, I went for it.

I’m still on that trial, so everything above works for now. Once it ends, the widget will go quiet, but by then this post will probably be long forgotten anyway.

Cover photo by Warren Sammut on Unsplash.

More polaroids I want to share

CLICK OR DRAG THE TOP ONE ASIDE

You've reached the end of the shoebox. More polaroids and other posts live on my Instagram.

@GMJELLE →
Polaroid of a calm lake at dusk reflecting pink clouds and dark hills, at a wild campsite near Adana, Turkey
Polaroid of Josip Broz Tito's marble tomb seen through plant leaves in Belgrade, Serbia
Polaroid of a motorcycle parked at a Soviet-era bus stop decorated with a flower mosaic in Moldova
Polaroid of a large stone statue of a bearded man in a long coat on a hillside near Buzludzha, Bulgaria
Polaroid of the saucer-shaped Buzludzha Monument on a distant hilltop above the trees in Bulgaria
Polaroid of a man standing in front of the Buzludzha Monument and its tower in Bulgaria
Polaroid of a valley of pale, cone-shaped rock formations in Cappadocia, Turkey
Polaroid of hot air balloons silhouetted against the dawn sky in Cappadocia, Turkey
Polaroid of the brutalist Cosmos Hotel tower in Chisinau, Moldova
Polaroid of the Tomb of Cyrus the Great on its stepped stone base at Pasargadae, Iran
Polaroid of a close view of the Tomb of Cyrus the Great at Pasargadae, Iran
Polaroid of a converted red fire truck camper and a motorcycle at a campsite on the west coast of Turkey
Polaroid of a mosque minaret and domes silhouetted against a cloudy sky in Edirne, Turkey
Polaroid of the Mimar Sinan statue in front of the Selimiye Mosque in Edirne, Turkey
Polaroid of the courtyard of a historic house with a fountain and colored-glass windows in Isfahan, Iran
Polaroid of a man standing next to an Isfahan city emblem sign on a sidewalk in Isfahan, Iran
Polaroid of the tiled dome and entrance portal of the Sheikh Lotfollah Mosque in Isfahan, Iran
Polaroid of the tiled entrance portal and twin minarets of the Shah Mosque in Isfahan, Iran
Polaroid of a columned palace silhouetted at sunset behind a fountain pool on Naqsh-e Jahan Square, Isfahan, Iran
Polaroid of a dark interior lit through colorful stained-glass windows in a historic house in Isfahan, Iran
Polaroid of rocky ridges and rock formations under a cloudy sky near Goreme, Cappadocia, Turkey
Polaroid of a loaded motorcycle parked by a desert road with mountains behind it in Iran
Polaroid of a loaded motorcycle with a helmet on the mirror, parked on a coastal road near Izmir, Turkey
Polaroid of a Soviet-era mosaic of workers and a motorcyclist on a building wall in Lutsk, Ukraine
Polaroid of a Soviet-era mosaic of a crowned woman holding scales, framed by tree leaves in Lviv, Ukraine
Polaroid of the Lviv Opera House facade with a fountain in front in Lviv, Ukraine
Polaroid of a Soviet WWII memorial column with a stork's nest on top in northern Moldova
Polaroid of a large Soviet-era mural of figures on a wall beside a tree in Moldova
Polaroid of royal tombs carved into the cliff face at Naqsh-e Rustam near Persepolis, Iran
Polaroid of a stone relief of a Persian soldier holding a spear at Persepolis, Iran
Polaroid of the Gate of All Nations with its winged bull statues and columns at Persepolis, Iran
Polaroid of four men sharing dinner at a table in a traditional hostel in Shiraz, Iran
Polaroid of the tiled courtyard and reflecting pool of the Nasir al-Mulk (Pink) Mosque in Shiraz, Iran
Polaroid of two men smiling at a restaurant table in Tabriz, Iran
Polaroid of the mirrored hall of Golestan Palace lit by large chandeliers in Tehran, Iran
Polaroid of the bronze statue of Leonidas raising a spear at the Thermopylae monument, Greece
Polaroid of a man looking up at the wooden Trojan Horse from the Troy film in Canakkale, Turkey
Polaroid of a backgammon board on a table overlooking the sea at a campsite in Turkey
Polaroid of a Soviet-era mosaic of a woman in traditional dress under a Ukrainian coat of arms in Ukraine
Polaroid of a motorcycle parked at a bus stop with a mosaic of three knights in Ukraine
Polaroid of a man standing on a boardwalk in front of the stairway of the Ziggurat of Ur, Iraq
Polaroid looking up the brick stairway of the Ziggurat of Ur, Iraq
Polaroid of eroded mud-brick ruins with a small sign in front at Uruk, Iraq
Polaroid of a man and two boys crouching by a freshly slaughtered sheep on grass on the road to Van, Turkey
Polaroid looking up at the curved glass Zlota 44 skyscraper in Warsaw, Poland
Polaroid of the Palace of Culture and Science lit at dusk in Warsaw, Poland
Polaroid of the Zoroastrian Fire Temple with a round reflecting pool in Yazd, Iran