Skip to content

Commit 96ef63f

Browse files
committed
Suffix WIP
1 parent 1c62dbb commit 96ef63f

8 files changed

Lines changed: 91 additions & 185 deletions

File tree

lib/plausible/ingestion/source.ex

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ defmodule Plausible.Ingestion.Source do
1515
|> then(&["adwords" | &1])
1616
|> MapSet.new()
1717

18+
@custom_source_suffixes %{
19+
"officeapps.live.com" => "Microsoft 365",
20+
"wikipedia.org" => "Wikipedia"
21+
}
22+
1823
@external_resource "priv/ref_inspector/referers.yml"
1924
@referers_yaml Application.app_dir(:plausible, "priv/ref_inspector/referers.yml")
2025

@@ -84,13 +89,19 @@ defmodule Plausible.Ingestion.Source do
8489
|> URI.parse()
8590
|> format_referrer_host()
8691

92+
downcased_host = String.downcase(host)
93+
8794
# Prefer custom source overrides over RefInspector so subdomain matches win
88-
# (e.g. gemini.google.com resolves to Google Gemini, not Google).
89-
case from_custom_sources(String.downcase(host)) do
90-
name when is_binary(name) ->
95+
# (e.g. gemini.google.com resolves to Google Gemini, not Google). Exact host
96+
# matches take priority, then suffix families, then RefInspector.
97+
cond do
98+
name = from_custom_sources(downcased_host) ->
99+
name
100+
101+
name = from_custom_source_suffix(downcased_host) ->
91102
name
92103

93-
_ ->
104+
true ->
94105
case RefInspector.parse(referrer).source do
95106
:unknown -> host
96107
# Normalize RefInspector names through our aliases (e.g. Twitter to X (Twitter)).
@@ -99,6 +110,12 @@ defmodule Plausible.Ingestion.Source do
99110
end
100111
end
101112

113+
defp from_custom_source_suffix(host) do
114+
Enum.find_value(@custom_source_suffixes, fn {suffix, name} ->
115+
if host == suffix or String.ends_with?(host, "." <> suffix), do: name
116+
end)
117+
end
118+
102119
defp tagged_param(request) do
103120
request.query_params["utm_source"] ||
104121
request.query_params["source"] ||

lib/plausible_web/plugs/favicon.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ defmodule PlausibleWeb.Favicon do
5151
"DeepSeek" => "deepseek.com",
5252
"Microsoft Copilot" => "copilot.microsoft.com",
5353
"Grok" => "grok.com",
54-
"X (Twitter)" => "x.com"
54+
"X (Twitter)" => "x.com",
55+
"Microsoft 365" => "office.com"
5556
}
5657

5758
def init(_) do

priv/custom_sources.json

Lines changed: 2 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -78,172 +78,7 @@
7878
"twitter": "X (Twitter)",
7979
"tiktokads": "TikTok",
8080
"tik.tok": "TikTok",
81-
"fo.wikipedia.org":"Wikipedia",
82-
"ga.wikipedia.org":"Wikipedia",
83-
"el.m.wikipedia.org":"Wikipedia",
84-
"eo.m.wikipedia.org":"Wikipedia",
85-
"ms.m.wikipedia.org":"Wikipedia",
86-
"nl.wikipedia.org":"Wikipedia",
87-
"dga.m.wikipedia.org":"Wikipedia",
88-
"th.wikipedia.org":"Wikipedia",
89-
"oc.wikipedia.org":"Wikipedia",
90-
"da.wikipedia.org":"Wikipedia",
91-
"pt.m.wikipedia.org":"Wikipedia",
92-
"szl.m.wikipedia.org":"Wikipedia",
93-
"be-tarask.wikipedia.org":"Wikipedia",
94-
"ta.m.wikipedia.org":"Wikipedia",
95-
"pa.m.wikipedia.org":"Wikipedia",
96-
"mn.wikipedia.org":"Wikipedia",
97-
"sv.m.wikipedia.org":"Wikipedia",
98-
"sk.wikipedia.org":"Wikipedia",
99-
"it.wikipedia.org":"Wikipedia",
100-
"el.wikipedia.org":"Wikipedia",
101-
"olo.wikipedia.org":"Wikipedia",
102-
"hi.m.wikipedia.org":"Wikipedia",
103-
"bn.m.wikipedia.org":"Wikipedia",
104-
"uz.wikipedia.org":"Wikipedia",
105-
"fr.m.wikipedia.org":"Wikipedia",
106-
"fa.wikipedia.org":"Wikipedia",
107-
"fi.wikipedia.org":"Wikipedia",
108-
"arz.m.wikipedia.org":"Wikipedia",
109-
"si.m.wikipedia.org":"Wikipedia",
110-
"bjn.wikipedia.org":"Wikipedia",
111-
"kn.wikipedia.org":"Wikipedia",
112-
"is.m.wikipedia.org":"Wikipedia",
113-
"nostalgia.wikipedia.org":"Wikipedia",
114-
"en.wikipedia.org":"Wikipedia",
115-
"nl.m.wikipedia.org":"Wikipedia",
116-
"nn.m.wikipedia.org":"Wikipedia",
117-
"bs.wikipedia.org":"Wikipedia",
118-
"sh.m.wikipedia.org":"Wikipedia",
119-
"vi.m.wikipedia.org":"Wikipedia",
120-
"ru.wikipedia.org":"Wikipedia",
121-
"tr.m.wikipedia.org":"Wikipedia",
122-
"he.wikipedia.org":"Wikipedia",
123-
"ta.wikipedia.org":"Wikipedia",
124-
"es.wikipedia.org":"Wikipedia",
125-
"si.wikipedia.org":"Wikipedia",
126-
"pl.wikipedia.org":"Wikipedia",
127-
"hu.wikipedia.org":"Wikipedia",
128-
"lij.m.wikipedia.org":"Wikipedia",
129-
"nn.wikipedia.org":"Wikipedia",
130-
"ko.m.wikipedia.org":"Wikipedia",
131-
"da.m.wikipedia.org":"Wikipedia",
132-
"zh.m.wikipedia.org":"Wikipedia",
133-
"vec.wikipedia.org":"Wikipedia",
134-
"ar.wikipedia.org":"Wikipedia",
135-
"bcl.m.wikipedia.org":"Wikipedia",
136-
"en.m.wikipedia.org":"Wikipedia",
137-
"sw.wikipedia.org":"Wikipedia",
138-
"la.m.wikipedia.org":"Wikipedia",
139-
"ur.m.wikipedia.org":"Wikipedia",
140-
"id.m.wikipedia.org":"Wikipedia",
141-
"crh.wikipedia.org":"Wikipedia",
142-
"sr.wikipedia.org":"Wikipedia",
143-
"sw.m.wikipedia.org":"Wikipedia",
144-
"ka.m.wikipedia.org":"Wikipedia",
145-
"lt.m.wikipedia.org":"Wikipedia",
146-
"fy.wikipedia.org":"Wikipedia",
147-
"ro.m.wikipedia.org":"Wikipedia",
148-
"hr.wikipedia.org":"Wikipedia",
149-
"mn.m.wikipedia.org":"Wikipedia",
150-
"pt.wikipedia.org":"Wikipedia",
151-
"it.m.wikipedia.org":"Wikipedia",
152-
"lv.m.wikipedia.org":"Wikipedia",
153-
"fa.m.wikipedia.org":"Wikipedia",
154-
"ja.wikipedia.org":"Wikipedia",
155-
"lv.wikipedia.org":"Wikipedia",
156-
"hu.m.wikipedia.org":"Wikipedia",
157-
"de.wikipedia.org":"Wikipedia",
158-
"uk.wikipedia.org":"Wikipedia",
159-
"ml.wikipedia.org":"Wikipedia",
160-
"te.m.wikipedia.org":"Wikipedia",
161-
"bg.wikipedia.org":"Wikipedia",
162-
"eu.wikipedia.org":"Wikipedia",
163-
"arz.wikipedia.org":"Wikipedia",
164-
"id.wikipedia.org":"Wikipedia",
165-
"mg.m.wikipedia.org":"Wikipedia",
166-
"sq.m.wikipedia.org":"Wikipedia",
167-
"ca.wikipedia.org":"Wikipedia",
168-
"sk.m.wikipedia.org":"Wikipedia",
169-
"az.wikipedia.org":"Wikipedia",
170-
"ru.m.wikipedia.org":"Wikipedia",
171-
"uz.m.wikipedia.org":"Wikipedia",
172-
"wuu.wikipedia.org":"Wikipedia",
173-
"hy.wikipedia.org":"Wikipedia",
174-
"la.wikipedia.org":"Wikipedia",
175-
"ca.m.wikipedia.org":"Wikipedia",
176-
"ckb.m.wikipedia.org":"Wikipedia",
177-
"tt.wikipedia.org":"Wikipedia",
178-
"gu.m.wikipedia.org":"Wikipedia",
179-
"lrc.wikipedia.org":"Wikipedia",
180-
"be-tarask.m.wikipedia.org":"Wikipedia",
181-
"no.m.wikipedia.org":"Wikipedia",
182-
"simple.m.wikipedia.org":"Wikipedia",
183-
"eu.m.wikipedia.org":"Wikipedia",
184-
"ne.m.wikipedia.org":"Wikipedia",
185-
"sr.m.wikipedia.org":"Wikipedia",
186-
"vi.wikipedia.org":"Wikipedia",
187-
"lt.wikipedia.org":"Wikipedia",
188-
"cs.m.wikipedia.org":"Wikipedia",
189-
"hy.m.wikipedia.org":"Wikipedia",
190-
"mr.wikipedia.org":"Wikipedia",
191-
"sv.wikipedia.org":"Wikipedia",
192-
"eo.wikipedia.org":"Wikipedia",
193-
"as.m.wikipedia.org":"Wikipedia",
194-
"is.wikipedia.org":"Wikipedia",
195-
"sh.wikipedia.org":"Wikipedia",
196-
"zh-classical.wikipedia.org":"Wikipedia",
197-
"nds-nl.m.wikipedia.org":"Wikipedia",
198-
"tl.m.wikipedia.org":"Wikipedia",
199-
"tr.wikipedia.org":"Wikipedia",
200-
"cs.wikipedia.org":"Wikipedia",
201-
"uk.m.wikipedia.org":"Wikipedia",
202-
"sq.wikipedia.org":"Wikipedia",
203-
"et.m.wikipedia.org":"Wikipedia",
204-
"hr.m.wikipedia.org":"Wikipedia",
205-
"bn.wikipedia.org":"Wikipedia",
206-
"sl.wikipedia.org":"Wikipedia",
207-
"th.m.wikipedia.org":"Wikipedia",
208-
"hi.wikipedia.org":"Wikipedia",
209-
"he.m.wikipedia.org":"Wikipedia",
210-
"bat-smg.wikipedia.org":"Wikipedia",
211-
"ml.m.wikipedia.org":"Wikipedia",
212-
"zh.wikipedia.org":"Wikipedia",
213-
"fi.m.wikipedia.org":"Wikipedia",
214-
"de.m.wikipedia.org":"Wikipedia",
215-
"be.wikipedia.org":"Wikipedia",
216-
"pl.m.wikipedia.org":"Wikipedia",
217-
"simple.wikipedia.org":"Wikipedia",
218-
"rw.m.wikipedia.org":"Wikipedia",
219-
"no.wikipedia.org":"Wikipedia",
220-
"ja.m.wikipedia.org":"Wikipedia",
221-
"yi.m.wikipedia.org":"Wikipedia",
222-
"ga.m.wikipedia.org":"Wikipedia",
223-
"ar.m.wikipedia.org":"Wikipedia",
81+
"wikipedia": "Wikipedia",
22482
"canary.discord.com":"Discord",
225-
"sa.m.wikipedia.org":"Wikipedia",
226-
"ky.wikipedia.org":"Wikipedia",
227-
"es.m.wikipedia.org":"Wikipedia",
228-
"new.wikipedia.org":"Wikipedia",
229-
"lij.wikipedia.org":"Wikipedia",
230-
"zh-yue.wikipedia.org":"Wikipedia",
231-
"bg.m.wikipedia.org":"Wikipedia",
232-
"bs.m.wikipedia.org":"Wikipedia",
233-
"dz.wikipedia.org":"Wikipedia",
234-
"kk.m.wikipedia.org":"Wikipedia",
235-
"fr.wikipedia.org":"Wikipedia",
236-
"qu.wikipedia.org":"Wikipedia",
237-
"ka.wikipedia.org":"Wikipedia",
238-
"webk.telegram.org":"Telegram",
239-
"et.wikipedia.org":"Wikipedia",
240-
"ms.wikipedia.org":"Wikipedia",
241-
"az.m.wikipedia.org":"Wikipedia",
242-
"cy.wikipedia.org":"Wikipedia",
243-
"ro.wikipedia.org":"Wikipedia",
244-
"mk.wikipedia.org":"Wikipedia",
245-
"tl.wikipedia.org":"Wikipedia",
246-
"am.wikipedia.org":"Wikipedia",
247-
"ko.wikipedia.org":"Wikipedia",
248-
"sl.m.wikipedia.org":"Wikipedia"
83+
"webk.telegram.org":"Telegram"
24984
}

priv/ingest_repo/migrations/20260601000001_remap_sources_v3.exs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,34 @@ defmodule Plausible.IngestRepo.Migrations.RemapSourcesV3 do
3939
"mstdn.jp" => "Mastodon"
4040
}
4141

42+
@suffix_mappings %{
43+
"officeapps.live.com" => "Microsoft 365",
44+
"wikipedia.org" => "Wikipedia"
45+
}
46+
4247
def up do
4348
{keys, values} = Enum.unzip(@mappings)
4449

45-
events_sql = """
46-
ALTER TABLE events_v2
47-
UPDATE referrer_source = transform(lower(referrer_source), {$0:Array(String)}, {$1:Array(String)})
48-
WHERE lower(referrer_source) IN {$0:Array(String)}
49-
"""
50+
for table <- ["events_v2", "sessions_v2"] do
51+
transform_sql = """
52+
ALTER TABLE #{table}
53+
UPDATE referrer_source = transform(lower(referrer_source), {$0:Array(String)}, {$1:Array(String)})
54+
WHERE lower(referrer_source) IN {$0:Array(String)}
55+
"""
56+
57+
execute(fn -> repo().query!(transform_sql, [keys, values]) end)
5058

51-
sessions_sql = """
52-
ALTER TABLE sessions_v2
53-
UPDATE referrer_source = transform(lower(referrer_source), {$0:Array(String)}, {$1:Array(String)})
54-
WHERE lower(referrer_source) IN {$0:Array(String)}
55-
"""
59+
for {suffix, name} <- @suffix_mappings do
60+
suffix_sql = """
61+
ALTER TABLE #{table}
62+
UPDATE referrer_source = {$0:String}
63+
WHERE referrer_source != {$0:String}
64+
AND (lower(referrer_source) = {$1:String} OR endsWith(lower(referrer_source), {$2:String}))
65+
"""
5666

57-
execute(fn -> repo().query!(events_sql, [keys, values]) end)
58-
execute(fn -> repo().query!(sessions_sql, [keys, values]) end)
67+
execute(fn -> repo().query!(suffix_sql, [name, suffix, "." <> suffix]) end)
68+
end
69+
end
5970
end
6071

6172
def down do

test/plausible/ingestion/acquisition_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ defmodule Plausible.Ingestion.AcquisitionTest do
113113
%{referrer_source: "Threads", expected: "Organic Social"},
114114
%{referrer_source: "Ecosia", expected: "Organic Search"},
115115
%{referrer_source: "Kagi", expected: "Organic Search"},
116+
%{referrer_source: "Microsoft 365", expected: "Referral"},
116117
%{
117118
referrer_source: "Google",
118119
utm_medium: "display",

test/plausible/ingestion/source_test.exs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,39 @@ defmodule Plausible.Ingestion.SourceTest do
100100
assert Source.resolve(%{@base_request | referrer: "https://pplx.ai"}) == "Perplexity"
101101
end
102102

103+
test "officeapps.live.com and its subdomains are Microsoft 365" do
104+
for referrer <- [
105+
"https://officeapps.live.com",
106+
"https://cac-excel.officeapps.live.com",
107+
"https://euc-excel.officeapps.live.com",
108+
"https://ukc-word-edit.officeapps.live.com"
109+
] do
110+
assert Source.resolve(%{@base_request | referrer: referrer}) == "Microsoft 365"
111+
end
112+
end
113+
114+
test "a host that only looks like the officeapps suffix is not Microsoft 365" do
115+
assert Source.resolve(%{@base_request | referrer: "https://notofficeapps.live.com"}) ==
116+
"notofficeapps.live.com"
117+
end
118+
119+
test "any wikipedia.org subdomain is Wikipedia (including unlisted language editions)" do
120+
for referrer <- [
121+
"https://wikipedia.org",
122+
"https://www.wikipedia.org",
123+
"https://en.wikipedia.org",
124+
"https://en.m.wikipedia.org",
125+
"https://zu.wikipedia.org"
126+
] do
127+
assert Source.resolve(%{@base_request | referrer: referrer}) == "Wikipedia"
128+
end
129+
end
130+
131+
test "a host that only looks like the wikipedia suffix is not Wikipedia" do
132+
assert Source.resolve(%{@base_request | referrer: "https://notwikipedia.org"}) ==
133+
"notwikipedia.org"
134+
end
135+
103136
test "RefInspector-resolved aliases are normalized (t.co referrer is 'X (Twitter)')" do
104137
# t.co is a Twitter domain known to RefInspector but not in custom_sources.json,
105138
# so it exercises the RefInspector branch and canonical mapping ('Twitter' to 'X (Twitter)').

test/plausible_web/controllers/api/external_controller_test.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2359,6 +2359,13 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
23592359
|> assert_source("Kagi")
23602360
|> assert_acquisition_channel("Organic Search")
23612361
end
2362+
2363+
test "officeapps.live.com subdomains are Microsoft 365 and Referral", %{site: site} do
2364+
site
2365+
|> event_with_referrer("https://cac-excel.officeapps.live.com")
2366+
|> assert_source("Microsoft 365")
2367+
|> assert_acquisition_channel("Referral")
2368+
end
23622369
end
23632370

23642371
describe "user_id generation" do

test/plausible_web/plugs/favicon_test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ defmodule PlausibleWeb.FaviconTest do
9999
"Bluesky" => "bsky.app",
100100
"Mastodon" => "mastodon.social",
101101
"X (Twitter)" => "x.com",
102-
"Kagi" => "kagi.com"
102+
"Kagi" => "kagi.com",
103+
"Microsoft 365" => "office.com"
103104
} do
104105
test "maps custom source #{source} to #{domain} for favicon", %{plug_opts: plug_opts} do
105106
expect(

0 commit comments

Comments
 (0)