Skip to content

Commit 6a9fe45

Browse files
authored
Script v2: Rate limit tech detection (#5701)
* Rate limit tech detection * Add tests * Fix tests * Unify rate limit key format * Move capture log to tags
1 parent db448d7 commit 6a9fe45

6 files changed

Lines changed: 138 additions & 63 deletions

File tree

lib/plausible/installation_support/detection/checks.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,26 @@ defmodule Plausible.InstallationSupport.Detection.Checks do
4343
state.url
4444
)
4545
end
46+
47+
@unthrottled_checks 3
48+
@first_slowdown_ms 1000
49+
def run_with_rate_limit(url, data_domain, opts \\ []) do
50+
case Plausible.RateLimit.check_rate(
51+
"site_detection:#{data_domain}",
52+
:timer.minutes(60),
53+
10
54+
) do
55+
{:allow, count} when count <= @unthrottled_checks ->
56+
{:ok, run(url, data_domain, opts)}
57+
58+
{:allow, count} when count > @unthrottled_checks ->
59+
# slowdown steps 1x, 4x, 9x, 16x, ...
60+
slowdown_ms = @first_slowdown_ms * (count - @unthrottled_checks) ** 2
61+
:timer.sleep(slowdown_ms)
62+
{:ok, run(url, data_domain, opts)}
63+
64+
{:deny, limit} ->
65+
{:error, {:rate_limit_exceeded, limit}}
66+
end
67+
end
4668
end

lib/plausible_web/live/change_domain_v2.ex

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -173,24 +173,22 @@ defmodule PlausibleWeb.Live.ChangeDomainV2 do
173173
end
174174

175175
defp run_detection(domain) do
176-
detection_result =
177-
Detection.Checks.run(nil, domain,
178-
detect_v1?: true,
179-
report_to: nil,
180-
async?: false,
181-
slowdown: 0
182-
)
183-
|> Detection.Checks.interpret_diagnostics()
184-
185-
case detection_result do
186-
%Result{
187-
ok?: true,
188-
data: data
189-
} ->
190-
{:ok, %{detection_result: data}}
191-
176+
with {:ok, detection_result} <-
177+
Detection.Checks.run_with_rate_limit(nil, domain,
178+
detect_v1?: true,
179+
report_to: nil,
180+
async?: false,
181+
slowdown: 0
182+
),
183+
%Result{ok?: true, data: data} <-
184+
Detection.Checks.interpret_diagnostics(detection_result) do
185+
{:ok, %{detection_result: data}}
186+
else
192187
%Result{ok?: false, errors: errors} ->
193188
{:error, List.first(errors, :unknown_reason)}
189+
190+
{:error, {:rate_limit_exceeded, _}} ->
191+
{:error, :rate_limit_exceeded}
194192
end
195193
end
196194
else

lib/plausible_web/live/installationv2.ex

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -206,24 +206,18 @@ defmodule PlausibleWeb.Live.InstallationV2 do
206206

207207
on_ee do
208208
defp detect_recommended_installation_type(flow, site) do
209-
detection_result =
210-
Detection.Checks.run(nil, site.domain,
211-
detect_v1?: flow == Flows.review(),
212-
report_to: nil,
213-
slowdown: 0,
214-
async?: false
215-
)
216-
|> Detection.Checks.interpret_diagnostics()
217-
218-
case detection_result do
219-
%Result{
220-
ok?: true,
221-
data: %{suggested_technology: suggested_technology, v1_detected: v1_detected}
222-
} ->
223-
{suggested_technology, v1_detected}
224-
225-
_ ->
226-
{"manual", false}
209+
with {:ok, detection_result} <-
210+
Detection.Checks.run_with_rate_limit(nil, site.domain,
211+
detect_v1?: flow == Flows.review(),
212+
report_to: nil,
213+
slowdown: 0,
214+
async?: false
215+
),
216+
%Result{ok?: true, data: data} <-
217+
Detection.Checks.interpret_diagnostics(detection_result) do
218+
{data.suggested_technology, data.v1_detected}
219+
else
220+
_ -> {"manual", false}
227221
end
228222
end
229223
else

lib/plausible_web/live/verification.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ defmodule PlausibleWeb.Live.Verification do
140140
{:noreply, socket}
141141
else
142142
case Plausible.RateLimit.check_rate(
143-
"site_verification_#{socket.assigns.domain}",
143+
"site_verification:#{socket.assigns.domain}",
144144
:timer.minutes(60),
145145
3
146146
) do

test/plausible_web/live/change_domain_v2_test.exs

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
44

55
import Phoenix.LiveViewTest
66
import Plausible.TestUtils
7-
import ExUnit.CaptureLog
87
import Plausible.Test.Support.HTML
98

109
on_ee do
@@ -88,7 +87,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
8887
end
8988

9089
original_domain = site.domain
91-
new_domain = "new-example.com"
90+
new_domain = "new.#{site.domain}"
9291
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
9392

9493
lv
@@ -111,14 +110,14 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
111110
end
112111

113112
original_domain = site.domain
114-
new_domain = "new-example.com"
113+
new_domain = "new.#{site.domain}"
115114
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
116115

117116
lv
118117
|> element("form")
119118
|> render_submit(%{site: %{domain: new_domain}})
120119

121-
assert_patch(lv, "/#{new_domain}/change-domain-v2/success")
120+
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
122121

123122
html = render_async(lv, 500)
124123
assert html =~ "Domain Changed Successfully"
@@ -164,14 +163,14 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
164163
"wordpressPlugin" => true
165164
})
166165

167-
new_domain = "new-example.com"
166+
new_domain = "new.#{site.domain}"
168167
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
169168

170169
lv
171170
|> element("form")
172171
|> render_submit(%{site: %{domain: new_domain}})
173172

174-
assert_patch(lv, "/#{new_domain}/change-domain-v2/success")
173+
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
175174

176175
html = render_async(lv, 500)
177176
assert html =~ "<i>must</i>"
@@ -197,14 +196,14 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
197196
"wordpressPlugin" => false
198197
})
199198

200-
new_domain = "new-example.com"
199+
new_domain = "new.#{site.domain}"
201200
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
202201

203202
lv
204203
|> element("form")
205204
|> render_submit(%{site: %{domain: new_domain}})
206205

207-
assert_patch(lv, "/#{new_domain}/change-domain-v2/success")
206+
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
208207

209208
html = render_async(lv, 500)
210209
assert html =~ "<i>must</i>"
@@ -228,14 +227,14 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
228227
"wordpressPlugin" => false
229228
})
230229

231-
new_domain = "new-example.com"
230+
new_domain = "new.#{site.domain}"
232231
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
233232

234233
lv
235234
|> element("form")
236235
|> render_submit(%{site: %{domain: new_domain}})
237236

238-
assert_patch(lv, "/#{new_domain}/change-domain-v2/success")
237+
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
239238

240239
html = render_async(lv, 500)
241240
refute html =~ "Additional Steps Required"
@@ -258,14 +257,14 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
258257
"wordpressPlugin" => false
259258
})
260259

261-
new_domain = "new-example.com"
260+
new_domain = "new.#{site.domain}"
262261
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
263262

264263
lv
265264
|> element("form")
266265
|> render_submit(%{site: %{domain: new_domain}})
267266

268-
assert_patch(lv, "/#{new_domain}/change-domain-v2/success")
267+
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
269268

270269
html = render_async(lv, 500)
271270
assert html =~ "<i>must</i>"
@@ -279,41 +278,78 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
279278
)
280279
end
281280

282-
@tag :ee_only
281+
@tag ee_only: true, capture_log: true
282+
test "ratelimit is respected: browserless request isn't made and the notice is generic", %{
283+
conn: conn,
284+
site: site
285+
} do
286+
new_domain = "new.#{site.domain}"
287+
288+
# exceed the rate limit for site detection
289+
Plausible.RateLimit.check_rate(
290+
Plausible.RateLimit,
291+
"site_detection:#{new_domain}",
292+
:timer.minutes(60),
293+
1,
294+
100
295+
)
296+
297+
# stub won't be used, if it were used, the output would be different
298+
stub_detection_result(%{
299+
"v1Detected" => false,
300+
"gtmLikely" => false,
301+
"wordpressLikely" => false,
302+
"wordpressPlugin" => false
303+
})
304+
305+
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
306+
307+
lv
308+
|> element("form")
309+
|> render_submit(%{site: %{domain: new_domain}})
310+
311+
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
312+
313+
html = render_async(lv, 500)
314+
assert html =~ "Additional Steps Required"
315+
assert html =~ "<i>must</i>"
316+
assert html =~ "also update the site"
317+
assert html =~ "Plausible Installation"
318+
end
319+
320+
@tag ee_only: true, capture_log: true
283321
test "success page handles detection error gracefully", %{conn: conn, site: site} do
284322
stub_detection_error()
285323

286-
capture_log(fn ->
287-
new_domain = "new-example.com"
288-
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
324+
new_domain = "new.#{site.domain}"
325+
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
289326

290-
lv
291-
|> element("form")
292-
|> render_submit(%{site: %{domain: new_domain}})
327+
lv
328+
|> element("form")
329+
|> render_submit(%{site: %{domain: new_domain}})
293330

294-
assert_patch(lv, "/#{new_domain}/change-domain-v2/success")
331+
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
295332

296-
html = render_async(lv, 500)
297-
assert html =~ "Additional Steps Required"
298-
assert html =~ "<i>must</i>"
299-
assert html =~ "also update the site"
300-
assert html =~ "Plausible Installation"
301-
end)
333+
html = render_async(lv, 500)
334+
assert html =~ "Additional Steps Required"
335+
assert html =~ "<i>must</i>"
336+
assert html =~ "also update the site"
337+
assert html =~ "Plausible Installation"
302338
end
303339

304340
@tag :ce_build_only
305341
test "success page shows generic v1 notice for CE", %{
306342
conn: conn,
307343
site: site
308344
} do
309-
new_domain = "new-example.com"
345+
new_domain = "new.#{site.domain}"
310346
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
311347

312348
lv
313349
|> element("form")
314350
|> render_submit(%{site: %{domain: new_domain}})
315351

316-
assert_patch(lv, "/#{new_domain}/change-domain-v2/success")
352+
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
317353

318354
html = render_async(lv, 500)
319355
notice = text_of_element(html, "div[data-testid='ce-generic-notice']")

test/plausible_web/live/installationv2_test.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,31 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
372372
assert text(html) =~ "We've detected your website is using WordPress"
373373
end
374374

375+
@tag :ee_only
376+
test "if ratelimit for detection is exceeded, does not make detection request and falls back to recommending manual installation",
377+
%{conn: conn, site: site} do
378+
stub_dns_lookup_a_records(site.domain)
379+
380+
# exceed the rate limit for site detection
381+
Plausible.RateLimit.check_rate(
382+
Plausible.RateLimit,
383+
"site_detection:#{site.domain}",
384+
:timer.minutes(60),
385+
1,
386+
100
387+
)
388+
389+
# this won't be used: if it were used, the output would be different
390+
stub_detection_wordpress()
391+
392+
{lv, _} = get_lv(conn, site)
393+
394+
html = render_async(lv, 500)
395+
396+
refute text(html) =~ "We've detected your website is using WordPress"
397+
assert text(html) =~ "Verify Script installation"
398+
end
399+
375400
@tag :ee_only
376401
test "detected GTM installation shows special message", %{conn: conn, site: site} do
377402
stub_dns_lookup_a_records(site.domain)

0 commit comments

Comments
 (0)