Source and medium
Where a visit or an order came from. Two free-text values arrive from upstream and CCC maps them onto a fixed set, which is what every acquisition figure is grouped by.
Source and medium is the acquisition dimension: google / cpc, newsletter / email, and so on. The values arrive as free text and would be unusable grouped as they came, so CCC keeps the raw pair and maps it onto its own controlled lists.
The two lists are CCC's own. Nothing upstream knows about them: GA4 has no idea that app.aiden.cx is the source Aiden here, and the webshop platform has no list of mediums to agree with. A raw value CCC does not hold falls back rather than creating a source of its own, which is what keeps the acquisition dimension a fixed set that a report can group by. So the lists only ever grow by somebody adding to them.
The same dimension sits on two objects: on traffic, per day and domain, and on a sales order, per order.
Where the raw values come from
| Population | Source | Note |
|---|---|---|
| Traffic per day | GA4, trafficacquisitionperday | Takes a date, so it backfills |
| Orders, recent | GA4, cm2sourcemedium | Takes no date: one rolling window, whatever day the batch carries |
| Orders, 2022 to 2025 | verkoopdump | A backfill, because GA4 cannot be asked for that history |
The order-level GA4 call answering one rolling window is the reason the second row and the third row both exist. verkoopdump/sourcemedium.php is the backfill that fills the years GA4 never reached. It runs from the # Historic section of the crontab, the section that holds the drains for windows that are imported once, and its cron is switched off again once that window is in.
Both order-level jobs write into salesordersourcemediumraw and then re-queue classify_salesorders for each affected order's date, because the order's classification carries the resolved source and medium and has to be rebuilt once it is known.
How a raw pair is resolved
In sourcemediums->buildsourcemediums(), in this order:
- An empty source or medium becomes
unknown. A blank is a value, not a gap. - A raw pair listed in
trafficsourcemediumshardmatchesis rewritten to a different pair before anything else is looked up. This is the escape hatch for combinations that cannot be fixed by mapping either half alone. - The source is looked up in
trafficsourceraw. No match falls back to Other, source 2. - The medium is taken from the source's forced medium if it has one, otherwise looked up in
trafficmediumraw. No match falls back to Unknown, medium 1.
A source can force a medium, and that wins over whatever medium arrived. Use it where a source is only ever reached one way and the upstream medium is unreliable, such as a comparison feed that is paid by definition. A platform that is reached both organically and through advertising must not carry one, because the forced medium would bury the difference.
Matching is exact. The lookups are plain string keys, so a raw value matches only its own spelling: no wildcards, no prefixes, and every country or subdomain variant of a source needs its own row. The jobs lowercase the raw pair before they store it, so the mapping tables hold lowercase.
Other and Unknown are not errors, and they do not mean the same thing. Other is a raw value that arrived and that nothing maps: a real acquisition channel nobody has told CCC about yet, and a growing count of it is the signal to add a mapping. Unknown on both halves is usually the opposite, an order that never received a source and medium at all, which is most of the history GA4 could not be asked about.
Where to look, and where to change
/entities/sourcemediums carries the whole chain, one tab per step. The first two answer what the mapping produced, the rest are the rules that produced it:
| Tab | Shows |
|---|---|
| Combinations | What orders were classified as, per period, which is what the margin reports group by. A row opens to show the raw pairs behind it |
| Not matched | Raw pairs nothing maps, so they fell back to Other or Unknown. The worklist |
| Sources | The controlled source list, with each source's forced medium |
| Sources (raw) | Raw source values and the source each maps to |
| Mediums | The controlled medium list |
| Mediums (raw) | Raw medium values and the medium each maps to |
| Hard matches | Raw pairs that are rewritten before mapping |
Combinations and Not matched read the classification the pipeline stored, not the mapping tables, so they cannot disagree with what the reports say. Opening a combination is how a mapping is checked: every raw pair underneath it should belong there. Not matched leaves out a pair a hard match already rewrites, because that is a decision somebody took rather than a gap.
The raw values are upstream data. The four mapping tables and the hard matches are CCC's own, and they are what you change when traffic or orders land in the wrong bucket. A mapping change applies to everything classified after it; already-classified orders keep their old answer until their date is re-queued.
Related
Evidence
Resolution order, the fallbacks and the forced medium from sourcemediums->buildsourcemediums(). Tabs from the switch(array_shift($URI)) in app/http/entities/sourcemediums.php and the tables in templates/main/entities/sourcemediums.tpl. What Combinations and Not matched read from sourcemediums->getcombinations(), getcombinationraw() and getnotmatched(). Exact matching and the lowercasing from the array lookups in buildsourcemediums() and the strtolower() in the three writing jobs. The rolling-window limitation and the backfill from AGENTS.md, The application, and app/jobs/verkoopdump/sourcemedium.php. Re-queueing of classify_salesorders from app/jobs/googleanalytics/getsourcemedium.php and that same backfill job. Basis: code-checked at 30f5313.
- Type
object - Status
active - Updated
2026-09-15 - Created
2026-09-13 - Covers
entities/sourcemediums