Leon4gr45 commited on
Commit
19cbdaf
·
verified ·
1 Parent(s): 3140cce

Delete test/plausible/stats/query/query_parse_and_build_test.exs with huggingface_hub

Browse files
test/plausible/stats/query/query_parse_and_build_test.exs DELETED
@@ -1,2804 +0,0 @@
1
- defmodule Plausible.Stats.Query.QueryParseAndBuildTest do
2
- use Plausible.DataCase
3
- import Plausible.AssertMatches
4
-
5
- alias Plausible.Stats.{Query, DateTimeRange, Filters, QueryError}
6
-
7
- @now DateTime.new!(~D[2021-05-05], ~T[12:30:00], "Etc/UTC")
8
- @date_range_today %DateTimeRange{
9
- first: DateTime.new!(~D[2021-05-05], ~T[00:00:00], "Etc/UTC"),
10
- last: @now
11
- }
12
- @date_range_day %DateTimeRange{
13
- first: DateTime.new!(~D[2021-05-05], ~T[00:00:00], "Etc/UTC"),
14
- last: DateTime.new!(~D[2021-05-05], ~T[23:59:59], "Etc/UTC")
15
- }
16
- @date_range_7d %DateTimeRange{
17
- first: DateTime.new!(~D[2021-04-28], ~T[00:00:00], "Etc/UTC"),
18
- last: DateTime.new!(~D[2021-05-04], ~T[23:59:59], "Etc/UTC")
19
- }
20
- @date_range_10d %DateTimeRange{
21
- first: DateTime.new!(~D[2021-04-25], ~T[00:00:00], "Etc/UTC"),
22
- last: DateTime.new!(~D[2021-05-04], ~T[23:59:59], "Etc/UTC")
23
- }
24
- @date_range_30d %DateTimeRange{
25
- first: DateTime.new!(~D[2021-04-05], ~T[00:00:00], "Etc/UTC"),
26
- last: DateTime.new!(~D[2021-05-04], ~T[23:59:59], "Etc/UTC")
27
- }
28
- @date_range_month %DateTimeRange{
29
- first: DateTime.new!(~D[2021-05-01], ~T[00:00:00], "Etc/UTC"),
30
- last: DateTime.new!(~D[2021-05-31], ~T[23:59:59], "Etc/UTC")
31
- }
32
- @date_range_3mo %DateTimeRange{
33
- first: DateTime.new!(~D[2021-02-01], ~T[00:00:00], "Etc/UTC"),
34
- last: DateTime.new!(~D[2021-04-30], ~T[23:59:59], "Etc/UTC")
35
- }
36
- @date_range_6mo %DateTimeRange{
37
- first: DateTime.new!(~D[2020-11-01], ~T[00:00:00], "Etc/UTC"),
38
- last: DateTime.new!(~D[2021-04-30], ~T[23:59:59], "Etc/UTC")
39
- }
40
- @date_range_year %DateTimeRange{
41
- first: DateTime.new!(~D[2021-01-01], ~T[00:00:00], "Etc/UTC"),
42
- last: DateTime.new!(~D[2021-12-31], ~T[23:59:59], "Etc/UTC")
43
- }
44
- @date_range_12mo %DateTimeRange{
45
- first: DateTime.new!(~D[2020-05-01], ~T[00:00:00], "Etc/UTC"),
46
- last: DateTime.new!(~D[2021-04-30], ~T[23:59:59], "Etc/UTC")
47
- }
48
-
49
- @default_include %Plausible.Stats.QueryInclude{
50
- imports: false,
51
- imports_meta: false,
52
- time_labels: false,
53
- total_rows: false,
54
- compare: nil,
55
- compare_match_day_of_week: false,
56
- legacy_time_on_page_cutoff: nil,
57
- trim_relative_date_range: false
58
- }
59
-
60
- setup [:create_user, :create_site]
61
-
62
- def check_goals(query, opts) do
63
- assert %Query{
64
- preloaded_goals: preloaded_goals,
65
- revenue_warning: revenue_warning,
66
- revenue_currencies: revenue_currencies
67
- } = query
68
-
69
- assert goal_names(preloaded_goals[:all]) ==
70
- Enum.sort(Keyword.get(opts, :preloaded_goals)[:all])
71
-
72
- assert goal_names(preloaded_goals[:matching_toplevel_filters]) ==
73
- Enum.sort(Keyword.get(opts, :preloaded_goals)[:matching_toplevel_filters])
74
-
75
- assert revenue_warning == Keyword.get(opts, :revenue_warning)
76
- assert revenue_currencies == Keyword.get(opts, :revenue_currencies)
77
- end
78
-
79
- defp goal_names(goals), do: Enum.map(goals, & &1.display_name) |> Enum.sort()
80
-
81
- describe "metrics" do
82
- test "public API does not recognize exit_rate metric", %{site: site} do
83
- params = %{
84
- "site_id" => site.domain,
85
- "metrics" => ["exit_rate"],
86
- "date_range" => "all"
87
- }
88
-
89
- assert {:error, %QueryError{message: error}} =
90
- Query.parse_and_build(site, params, now: @now)
91
-
92
- assert error =~ "Invalid metric"
93
- end
94
-
95
- test "public API does not recognize total_visitors metric", %{site: site} do
96
- params = %{
97
- "site_id" => site.domain,
98
- "metrics" => ["total_visitors"],
99
- "date_range" => "all"
100
- }
101
-
102
- assert {:error, %QueryError{message: error}} =
103
- Query.parse_and_build(site, params, now: @now)
104
-
105
- assert error =~ "Invalid metric"
106
- end
107
-
108
- test "valid metrics passed", %{site: site} do
109
- params = %{
110
- "site_id" => site.domain,
111
- "metrics" => ["visitors", "events"],
112
- "date_range" => "all"
113
- }
114
-
115
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
116
-
117
- assert_matches %Query{
118
- metrics: [:visitors, :events],
119
- utc_time_range: ^@date_range_day,
120
- filters: [],
121
- dimensions: [],
122
- order_by: nil,
123
- timezone: ^site.timezone,
124
- include: ^@default_include,
125
- pagination: %{limit: 10_000, offset: 0}
126
- } = query
127
- end
128
-
129
- test "fuller list of metrics", %{site: site} do
130
- params = %{
131
- "site_id" => site.domain,
132
- "metrics" => [
133
- "visitors",
134
- "pageviews",
135
- "visits",
136
- "events",
137
- "bounce_rate",
138
- "visit_duration"
139
- ],
140
- "date_range" => "all"
141
- }
142
-
143
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
144
-
145
- assert_matches %Query{
146
- metrics: [
147
- :visitors,
148
- :pageviews,
149
- :visits,
150
- :events,
151
- :bounce_rate,
152
- :visit_duration
153
- ],
154
- utc_time_range: ^@date_range_day,
155
- filters: [],
156
- dimensions: [],
157
- order_by: nil,
158
- timezone: ^site.timezone,
159
- include: ^@default_include,
160
- pagination: %{limit: 10_000, offset: 0}
161
- } = query
162
- end
163
-
164
- test "same metric queried multiple times", %{site: site} do
165
- params = %{
166
- "site_id" => site.domain,
167
- "metrics" => ["events", "visitors", "visitors"],
168
- "date_range" => "all"
169
- }
170
-
171
- assert {:error,
172
- %QueryError{
173
- code: :failed_schema_validation,
174
- message: "#/metrics: Expected items to be unique but they were not."
175
- }} =
176
- Query.parse_and_build(site, params, now: @now)
177
- end
178
-
179
- test "no metrics passed", %{site: site} do
180
- params = %{
181
- "site_id" => site.domain,
182
- "metrics" => [],
183
- "date_range" => "all"
184
- }
185
-
186
- assert {:error,
187
- %QueryError{
188
- code: :failed_schema_validation,
189
- message: "#/metrics: Expected a minimum of 1 items but got 0."
190
- }} =
191
- Query.parse_and_build(site, params, now: @now)
192
- end
193
- end
194
-
195
- describe "filters validation" do
196
- for operation <- [
197
- :is,
198
- :is_not,
199
- :matches,
200
- :matches_not,
201
- :contains,
202
- :contains_not
203
- ] do
204
- test "#{operation} filter", %{site: site} do
205
- params = %{
206
- "site_id" => site.domain,
207
- "metrics" => ["visitors"],
208
- "date_range" => "all",
209
- "filters" => [
210
- [Atom.to_string(unquote(operation)), "event:name", ["foo"]]
211
- ]
212
- }
213
-
214
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
215
-
216
- assert_matches %Query{
217
- metrics: [:visitors],
218
- utc_time_range: ^@date_range_day,
219
- filters: [
220
- [^unquote(operation), "event:name", ["foo"]]
221
- ],
222
- dimensions: [],
223
- order_by: nil,
224
- timezone: ^site.timezone,
225
- include: ^@default_include,
226
- pagination: %{limit: 10_000, offset: 0}
227
- } = query
228
- end
229
-
230
- test "#{operation} filter with invalid clause", %{site: site} do
231
- params = %{
232
- "site_id" => site.domain,
233
- "metrics" => ["visitors"],
234
- "date_range" => "all",
235
- "filters" => [
236
- [Atom.to_string(unquote(operation)), "event:name", "foo"]
237
- ]
238
- }
239
-
240
- assert {:error, %QueryError{message: error}} =
241
- Query.parse_and_build(site, params, now: @now)
242
-
243
- assert error ==
244
- "#/filters/0: Invalid filter [\"#{unquote(operation)}\", \"event:name\", \"foo\"]"
245
- end
246
- end
247
-
248
- for operation <- [:matches_wildcard, :matches_wildcard_not] do
249
- test "#{operation} is not a valid filter operation in public API", %{site: site} do
250
- params = %{
251
- "site_id" => site.domain,
252
- "metrics" => ["visitors"],
253
- "date_range" => "all",
254
- "filters" => [
255
- [Atom.to_string(unquote(operation)), "event:name", ["foo"]]
256
- ]
257
- }
258
-
259
- assert {:error, %QueryError{message: error}} =
260
- Query.parse_and_build(site, params, now: @now)
261
-
262
- assert error ==
263
- "#/filters/0: Invalid filter [\"#{unquote(operation)}\", \"event:name\", [\"foo\"]]"
264
- end
265
- end
266
-
267
- for too_short_filter <- [
268
- [],
269
- ["and"],
270
- ["or"],
271
- ["and", []],
272
- ["or", []],
273
- ["not"],
274
- ["is_not"],
275
- ["is_not", "event:name"],
276
- ["has_done"],
277
- ["has_not_done"]
278
- ] do
279
- test "errors on too short filter #{inspect(too_short_filter)}", %{
280
- site: site
281
- } do
282
- params = %{
283
- "site_id" => site.domain,
284
- "metrics" => ["visitors"],
285
- "date_range" => "all",
286
- "filters" => [
287
- unquote(too_short_filter)
288
- ]
289
- }
290
-
291
- assert {:error, %QueryError{message: error}} =
292
- Query.parse_and_build(site, params, now: @now)
293
-
294
- assert error == ~s(#/filters/0: Invalid filter #{inspect(unquote(too_short_filter))})
295
- end
296
- end
297
-
298
- valid_filter = ["is", "event:props:foobar", ["value"]]
299
-
300
- for too_long_filter <- [
301
- ["and", [valid_filter], "extra"],
302
- ["or", [valid_filter], []],
303
- ["not", valid_filter, 1],
304
- Enum.concat(valid_filter, [true])
305
- ] do
306
- test "errors on too long filter #{inspect(too_long_filter)}", %{
307
- site: site
308
- } do
309
- params = %{
310
- "site_id" => site.domain,
311
- "metrics" => ["visitors"],
312
- "date_range" => "all",
313
- "filters" => [
314
- unquote(too_long_filter)
315
- ]
316
- }
317
-
318
- assert {:error, %QueryError{message: error}} =
319
- Query.parse_and_build(site, params, now: @now)
320
-
321
- assert error == ~s(#/filters/0: Invalid filter #{inspect(unquote(too_long_filter))})
322
- end
323
- end
324
-
325
- test "filtering by invalid operation", %{site: site} do
326
- params = %{
327
- "site_id" => site.domain,
328
- "metrics" => ["visitors"],
329
- "date_range" => "all",
330
- "filters" => [
331
- ["exists?", "event:name", ["foo"]]
332
- ]
333
- }
334
-
335
- assert {:error, %QueryError{message: error}} =
336
- Query.parse_and_build(site, params, now: @now)
337
-
338
- assert error == "#/filters/0: Invalid filter [\"exists?\", \"event:name\", [\"foo\"]]"
339
- end
340
-
341
- test "filtering by custom properties", %{site: site} do
342
- params = %{
343
- "site_id" => site.domain,
344
- "metrics" => ["visitors"],
345
- "date_range" => "all",
346
- "filters" => [
347
- ["is", "event:props:foobar", ["value"]]
348
- ]
349
- }
350
-
351
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
352
-
353
- assert_matches %Query{
354
- metrics: [:visitors],
355
- utc_time_range: ^@date_range_day,
356
- filters: [
357
- [:is, "event:props:foobar", ["value"]]
358
- ],
359
- dimensions: [],
360
- order_by: nil,
361
- timezone: ^site.timezone,
362
- include: ^@default_include,
363
- pagination: %{limit: 10_000, offset: 0}
364
- } = query
365
- end
366
-
367
- for dimension <- Filters.event_props() do
368
- if dimension != "goal" do
369
- test "filtering by event:#{dimension} filter", %{site: site} do
370
- prefixed_dimension = "event:#{unquote(dimension)}"
371
-
372
- params = %{
373
- "site_id" => site.domain,
374
- "metrics" => ["visitors"],
375
- "date_range" => "all",
376
- "filters" => [
377
- ["is", prefixed_dimension, ["foo"]]
378
- ]
379
- }
380
-
381
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
382
-
383
- assert_matches %Query{
384
- metrics: [:visitors],
385
- utc_time_range: ^@date_range_day,
386
- filters: [
387
- [:is, ^prefixed_dimension, ["foo"]]
388
- ],
389
- dimensions: [],
390
- order_by: nil,
391
- timezone: ^site.timezone,
392
- include: ^@default_include,
393
- pagination: %{limit: 10_000, offset: 0}
394
- } = query
395
- end
396
- end
397
- end
398
-
399
- for dimension <- Filters.visit_props() do
400
- test "filtering by visit:#{dimension} filter", %{site: site} do
401
- prefixed_dimension = "visit:#{unquote(dimension)}"
402
-
403
- params = %{
404
- "site_id" => site.domain,
405
- "metrics" => ["visitors"],
406
- "date_range" => "all",
407
- "filters" => [
408
- ["is", prefixed_dimension, ["ab"]]
409
- ]
410
- }
411
-
412
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
413
-
414
- assert_matches %Query{
415
- metrics: [:visitors],
416
- utc_time_range: ^@date_range_day,
417
- filters: [
418
- [:is, ^prefixed_dimension, ["ab"]]
419
- ],
420
- dimensions: [],
421
- order_by: nil,
422
- timezone: ^site.timezone,
423
- include: ^@default_include,
424
- pagination: %{limit: 10_000, offset: 0}
425
- } = query
426
- end
427
- end
428
-
429
- test "invalid event filter", %{site: site} do
430
- params = %{
431
- "site_id" => site.domain,
432
- "metrics" => ["visitors"],
433
- "date_range" => "all",
434
- "filters" => [
435
- ["is", "event:device", ["foo"]]
436
- ]
437
- }
438
-
439
- assert {:error, %QueryError{message: error}} =
440
- Query.parse_and_build(site, params, now: @now)
441
-
442
- assert error == "#/filters/0: Invalid filter [\"is\", \"event:device\", [\"foo\"]]"
443
- end
444
-
445
- test "invalid visit filter", %{site: site} do
446
- params = %{
447
- "site_id" => site.domain,
448
- "metrics" => ["visitors"],
449
- "date_range" => "all",
450
- "filters" => [
451
- ["is", "visit:name", ["foo"]]
452
- ]
453
- }
454
-
455
- assert {:error, %QueryError{message: error}} =
456
- Query.parse_and_build(site, params, now: @now)
457
-
458
- assert error == "#/filters/0: Invalid filter [\"is\", \"visit:name\", [\"foo\"]]"
459
- end
460
-
461
- test "invalid filter", %{site: site} do
462
- params = %{
463
- "site_id" => site.domain,
464
- "metrics" => ["visitors"],
465
- "date_range" => "all",
466
- "filters" => "foobar"
467
- }
468
-
469
- assert {:error, %QueryError{message: error}} =
470
- Query.parse_and_build(site, params, now: @now)
471
-
472
- assert error == "#/filters: Type mismatch. Expected Array but got String."
473
- end
474
-
475
- test "numeric filter is invalid", %{site: site} do
476
- params = %{
477
- "site_id" => site.domain,
478
- "metrics" => ["visitors"],
479
- "date_range" => "all",
480
- "filters" => [["is", "visit:os_version", [123]]]
481
- }
482
-
483
- assert {:error, %QueryError{message: error}} =
484
- Query.parse_and_build(site, params, now: @now)
485
-
486
- assert error == "Invalid filter '[\"is\", \"visit:os_version\", [123]]'."
487
- end
488
-
489
- test "numbers are valid for visit:city", %{site: site} do
490
- params = %{
491
- "site_id" => site.domain,
492
- "metrics" => ["visitors"],
493
- "date_range" => "all",
494
- "filters" => [["is", "visit:city", [123, 456]]]
495
- }
496
-
497
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
498
-
499
- assert_matches %Query{
500
- metrics: [:visitors],
501
- utc_time_range: ^@date_range_day,
502
- filters: [
503
- [:is, "visit:city", [123, 456]]
504
- ],
505
- dimensions: [],
506
- order_by: nil,
507
- timezone: ^site.timezone,
508
- include: ^@default_include,
509
- pagination: %{limit: 10_000, offset: 0}
510
- } = query
511
- end
512
-
513
- test "strings are valid for visit:city", %{site: site} do
514
- params = %{
515
- "site_id" => site.domain,
516
- "metrics" => ["visitors"],
517
- "date_range" => "all",
518
- "filters" => [["is", "visit:city", ["123", "456"]]]
519
- }
520
-
521
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
522
-
523
- assert_matches %Query{
524
- metrics: [:visitors],
525
- utc_time_range: ^@date_range_day,
526
- filters: [
527
- [:is, "visit:city", ["123", "456"]]
528
- ],
529
- dimensions: [],
530
- order_by: nil,
531
- timezone: ^site.timezone,
532
- include: ^@default_include,
533
- pagination: %{limit: 10_000, offset: 0}
534
- } = query
535
- end
536
-
537
- test "invalid visit:country filter", %{site: site} do
538
- params = %{
539
- "site_id" => site.domain,
540
- "metrics" => ["visitors"],
541
- "date_range" => "all",
542
- "filters" => [["is", "visit:country", ["USA"]]]
543
- }
544
-
545
- assert {:error, %QueryError{message: error}} =
546
- Query.parse_and_build(site, params, now: @now)
547
-
548
- assert error ==
549
- "Invalid visit:country filter, visit:country needs to be a valid 2-letter country code."
550
- end
551
-
552
- test "valid nested `not`, `and` and `or`", %{site: site} do
553
- params = %{
554
- "site_id" => site.domain,
555
- "metrics" => ["visitors"],
556
- "date_range" => "all",
557
- "filters" => [
558
- [
559
- "or",
560
- [
561
- [
562
- "and",
563
- [
564
- ["is", "visit:city_name", ["Tallinn"]],
565
- ["is", "visit:country_name", ["Estonia"]]
566
- ]
567
- ],
568
- ["not", ["is", "visit:country_name", ["Estonia"]]]
569
- ]
570
- ]
571
- ]
572
- }
573
-
574
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
575
-
576
- assert_matches %Query{
577
- metrics: [:visitors],
578
- utc_time_range: ^@date_range_day,
579
- filters: [
580
- [
581
- :or,
582
- [
583
- [
584
- :and,
585
- [
586
- [:is, "visit:city_name", ["Tallinn"]],
587
- [:is, "visit:country_name", ["Estonia"]]
588
- ]
589
- ],
590
- [:not, [:is, "visit:country_name", ["Estonia"]]]
591
- ]
592
- ]
593
- ],
594
- dimensions: [],
595
- order_by: nil,
596
- timezone: ^site.timezone,
597
- include: ^@default_include,
598
- pagination: %{limit: 10_000, offset: 0}
599
- } = query
600
- end
601
-
602
- test "valid has_done and has_not_done filters", %{site: site} do
603
- insert(:goal, %{site: site, event_name: "Signup"})
604
-
605
- params = %{
606
- "site_id" => site.domain,
607
- "metrics" => ["visitors"],
608
- "date_range" => "all",
609
- "filters" => [
610
- ["has_done", ["is", "event:name", ["Signup"]]],
611
- [
612
- "has_not_done",
613
- [
614
- "or",
615
- [
616
- ["is", "event:goal", ["Signup"]],
617
- ["is", "event:page", ["/signup"]]
618
- ]
619
- ]
620
- ]
621
- ]
622
- }
623
-
624
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
625
-
626
- assert_matches %Query{
627
- metrics: [:visitors],
628
- utc_time_range: ^@date_range_day,
629
- filters: [
630
- [:has_done, [:is, "event:name", ["Signup"]]],
631
- [
632
- :has_not_done,
633
- [
634
- :or,
635
- [[:is, "event:goal", ["Signup"]], [:is, "event:page", ["/signup"]]]
636
- ]
637
- ]
638
- ],
639
- dimensions: [],
640
- order_by: nil,
641
- timezone: ^site.timezone,
642
- include: ^@default_include,
643
- pagination: %{limit: 10_000, offset: 0}
644
- } = query
645
- end
646
-
647
- test "fails when using visit filters within has_done filters", %{site: site} do
648
- params = %{
649
- "site_id" => site.domain,
650
- "metrics" => ["visitors"],
651
- "date_range" => "all",
652
- "filters" => [
653
- ["has_done", ["is", "visit:browser", ["Chrome"]]]
654
- ]
655
- }
656
-
657
- assert {:error, %QueryError{message: error}} =
658
- Query.parse_and_build(site, params, now: @now)
659
-
660
- assert error ==
661
- "Invalid filters. Behavioral filters (has_done, has_not_done) can only be used with event dimension filters."
662
- end
663
-
664
- test "fails when nesting behavioral filters", %{site: site} do
665
- params = %{
666
- "site_id" => site.domain,
667
- "metrics" => ["visitors"],
668
- "date_range" => "all",
669
- "filters" => [
670
- ["has_done", ["has_not_done", ["is", "visit:browser", ["Chrome"]]]]
671
- ]
672
- }
673
-
674
- assert {:error, %QueryError{message: error}} =
675
- Query.parse_and_build(site, params, now: @now)
676
-
677
- assert error ==
678
- "Invalid filters. Behavioral filters (has_done, has_not_done) cannot be nested."
679
- end
680
-
681
- for operator <- ["not", "or", "has_done", "has_not_done"] do
682
- test "invalid `#{operator}` clause", %{site: site} do
683
- params = %{
684
- "site_id" => site.domain,
685
- "metrics" => ["visitors"],
686
- "date_range" => "all",
687
- "filters" => [[unquote(operator), []]]
688
- }
689
-
690
- assert {:error, %QueryError{message: error}} =
691
- Query.parse_and_build(site, params, now: @now)
692
-
693
- assert error == "#/filters/0: Invalid filter [\"#{unquote(operator)}\", []]"
694
- end
695
- end
696
-
697
- test "event:hostname filter", %{site: site} do
698
- params = %{
699
- "site_id" => site.domain,
700
- "metrics" => ["visitors"],
701
- "date_range" => "all",
702
- "filters" => [["is", "event:hostname", ["a.plausible.io"]]]
703
- }
704
-
705
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
706
-
707
- assert_matches %Query{
708
- metrics: [:visitors],
709
- utc_time_range: ^@date_range_day,
710
- filters: [
711
- [:is, "event:hostname", ["a.plausible.io"]]
712
- ],
713
- dimensions: [],
714
- order_by: nil,
715
- timezone: ^site.timezone,
716
- include: ^@default_include,
717
- pagination: %{limit: 10_000, offset: 0}
718
- } = query
719
- end
720
-
721
- test "event:hostname filter not at top level is invalid", %{site: site} do
722
- params = %{
723
- "site_id" => site.domain,
724
- "metrics" => ["visitors"],
725
- "date_range" => "all",
726
- "filters" => [["not", ["is", "event:hostname", ["a.plausible.io"]]]]
727
- }
728
-
729
- assert {:error, %QueryError{message: error}} =
730
- Query.parse_and_build(site, params, now: @now)
731
-
732
- assert error ==
733
- "Invalid filters. Dimension `event:hostname` can only be filtered at the top level."
734
- end
735
-
736
- for operation <- [:is, :contains, :is_not, :contains_not] do
737
- test "#{operation} allows case_sensitive modifier", %{site: site} do
738
- params = %{
739
- "site_id" => site.domain,
740
- "metrics" => ["visitors"],
741
- "date_range" => "all",
742
- "filters" => [
743
- [
744
- Atom.to_string(unquote(operation)),
745
- "event:page",
746
- ["/foo"],
747
- %{"case_sensitive" => false}
748
- ],
749
- [
750
- Atom.to_string(unquote(operation)),
751
- "event:name",
752
- ["/foo"],
753
- %{"case_sensitive" => true}
754
- ]
755
- ]
756
- }
757
-
758
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
759
-
760
- assert_matches %Query{
761
- metrics: [:visitors],
762
- utc_time_range: ^@date_range_day,
763
- filters: [
764
- [
765
- ^unquote(operation),
766
- "event:page",
767
- ["/foo"],
768
- %{case_sensitive: false}
769
- ],
770
- [^unquote(operation), "event:name", ["/foo"], %{case_sensitive: true}]
771
- ],
772
- dimensions: [],
773
- order_by: nil,
774
- timezone: ^site.timezone,
775
- include: ^@default_include,
776
- pagination: %{limit: 10_000, offset: 0}
777
- } = query
778
- end
779
- end
780
-
781
- for operation <- [:matches, :matches_not] do
782
- test "case_sensitive modifier is not valid for #{operation}", %{site: site} do
783
- params = %{
784
- "site_id" => site.domain,
785
- "metrics" => ["visitors"],
786
- "date_range" => "all",
787
- "filters" => [
788
- [
789
- Atom.to_string(unquote(operation)),
790
- "event:hostname",
791
- ["a.plausible.io"],
792
- %{"case_sensitive" => false}
793
- ]
794
- ]
795
- }
796
-
797
- assert {:error, %QueryError{message: error}} =
798
- Query.parse_and_build(site, params, now: @now)
799
-
800
- assert error ==
801
- "#/filters/0: Invalid filter [\"#{unquote(operation)}\", \"event:hostname\", [\"a.plausible.io\"], %{\"case_sensitive\" => false}]"
802
- end
803
- end
804
- end
805
-
806
- describe "preloading goals" do
807
- setup %{site: site} do
808
- insert(:goal, %{site: site, event_name: "Signup"})
809
- insert(:goal, %{site: site, event_name: "Purchase"})
810
- insert(:goal, %{site: site, event_name: "Contact"})
811
-
812
- :ok
813
- end
814
-
815
- test "with exact match", %{site: site} do
816
- params = %{
817
- "site_id" => site.domain,
818
- "metrics" => ["visitors"],
819
- "date_range" => "all",
820
- "filters" => [["is", "event:goal", ["Signup", "Purchase"]]]
821
- }
822
-
823
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
824
-
825
- assert_matches %Query{
826
- metrics: [:visitors],
827
- utc_time_range: ^@date_range_day,
828
- filters: [[:is, "event:goal", ["Signup", "Purchase"]]],
829
- dimensions: [],
830
- order_by: nil,
831
- timezone: ^site.timezone,
832
- include: ^@default_include,
833
- pagination: %{limit: 10_000, offset: 0}
834
- } = query
835
-
836
- check_goals(query,
837
- preloaded_goals: %{
838
- all: ["Contact", "Purchase", "Signup"],
839
- matching_toplevel_filters: ["Purchase", "Signup"]
840
- },
841
- revenue_currencies: %{}
842
- )
843
- end
844
-
845
- test "with case insensitive match", %{site: site} do
846
- params = %{
847
- "site_id" => site.domain,
848
- "metrics" => ["visitors"],
849
- "date_range" => "all",
850
- "filters" => [["is", "event:goal", ["signup", "purchase"], %{"case_sensitive" => false}]]
851
- }
852
-
853
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
854
-
855
- assert_matches %Query{
856
- metrics: [:visitors],
857
- utc_time_range: ^@date_range_day,
858
- filters: [
859
- [:is, "event:goal", ["signup", "purchase"], %{case_sensitive: false}]
860
- ],
861
- dimensions: [],
862
- order_by: nil,
863
- timezone: ^site.timezone,
864
- include: ^@default_include,
865
- pagination: %{limit: 10_000, offset: 0}
866
- } = query
867
-
868
- check_goals(query,
869
- preloaded_goals: %{
870
- all: ["Contact", "Purchase", "Signup"],
871
- matching_toplevel_filters: ["Purchase", "Signup"]
872
- },
873
- revenue_currencies: %{}
874
- )
875
- end
876
-
877
- test "with contains match", %{site: site} do
878
- params = %{
879
- "site_id" => site.domain,
880
- "metrics" => ["visitors"],
881
- "date_range" => "all",
882
- "filters" => [["contains", "event:goal", ["Sign", "pur"]]]
883
- }
884
-
885
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
886
-
887
- assert_matches %Query{
888
- metrics: [:visitors],
889
- utc_time_range: ^@date_range_day,
890
- filters: [[:contains, "event:goal", ["Sign", "pur"]]],
891
- dimensions: [],
892
- order_by: nil,
893
- timezone: ^site.timezone,
894
- include: ^@default_include,
895
- pagination: %{limit: 10_000, offset: 0}
896
- } = query
897
-
898
- check_goals(query,
899
- preloaded_goals: %{
900
- all: ["Contact", "Purchase", "Signup"],
901
- matching_toplevel_filters: ["Signup"]
902
- },
903
- revenue_currencies: %{}
904
- )
905
- end
906
-
907
- test "with case insensitive contains match", %{site: site} do
908
- params = %{
909
- "site_id" => site.domain,
910
- "metrics" => ["visitors"],
911
- "date_range" => "all",
912
- "filters" => [["contains", "event:goal", ["sign", "CONT"], %{"case_sensitive" => false}]]
913
- }
914
-
915
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
916
-
917
- assert_matches %Query{
918
- metrics: [:visitors],
919
- utc_time_range: ^@date_range_day,
920
- filters: [
921
- [:contains, "event:goal", ["sign", "CONT"], %{case_sensitive: false}]
922
- ],
923
- dimensions: [],
924
- order_by: nil,
925
- timezone: ^site.timezone,
926
- include: ^@default_include,
927
- pagination: %{limit: 10_000, offset: 0}
928
- } = query
929
-
930
- check_goals(query,
931
- preloaded_goals: %{
932
- all: ["Contact", "Purchase", "Signup"],
933
- matching_toplevel_filters: ["Contact", "Signup"]
934
- },
935
- revenue_currencies: %{}
936
- )
937
- end
938
- end
939
-
940
- describe "include validation" do
941
- test "setting include values", %{site: site} do
942
- params = %{
943
- "site_id" => site.domain,
944
- "metrics" => ["visitors"],
945
- "date_range" => "all",
946
- "dimensions" => ["time"],
947
- "include" => %{"imports" => true, "time_labels" => true, "total_rows" => true}
948
- }
949
-
950
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
951
-
952
- assert_matches %Query{
953
- metrics: [:visitors],
954
- utc_time_range: ^@date_range_day,
955
- filters: [],
956
- dimensions: ["time"],
957
- order_by: nil,
958
- timezone: ^site.timezone,
959
- include: %{
960
- imports: true,
961
- imports_meta: false,
962
- time_labels: true,
963
- total_rows: true,
964
- compare: nil,
965
- compare_match_day_of_week: false,
966
- legacy_time_on_page_cutoff: nil,
967
- trim_relative_date_range: false
968
- },
969
- pagination: %{limit: 10_000, offset: 0}
970
- } = query
971
- end
972
-
973
- test "setting invalid imports value", %{site: site} do
974
- params = %{
975
- "site_id" => site.domain,
976
- "metrics" => ["visitors"],
977
- "date_range" => "all",
978
- "include" => "foobar"
979
- }
980
-
981
- assert {:error, %QueryError{message: error}} =
982
- Query.parse_and_build(site, params, now: @now)
983
-
984
- assert error == "#/include: Type mismatch. Expected Object but got String."
985
- end
986
-
987
- test "setting include.time_labels without time dimension", %{site: site} do
988
- params = %{
989
- "site_id" => site.domain,
990
- "metrics" => ["visitors"],
991
- "date_range" => "all",
992
- "include" => %{"time_labels" => true}
993
- }
994
-
995
- assert {:error, %QueryError{message: error}} =
996
- Query.parse_and_build(site, params, now: @now)
997
-
998
- assert error == "Invalid include.time_labels: requires a time dimension."
999
- end
1000
- end
1001
-
1002
- describe "pagination validation" do
1003
- test "setting pagination values", %{site: site} do
1004
- params = %{
1005
- "site_id" => site.domain,
1006
- "metrics" => ["visitors"],
1007
- "date_range" => "all",
1008
- "dimensions" => ["time"],
1009
- "pagination" => %{"limit" => 100, "offset" => 200}
1010
- }
1011
-
1012
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1013
-
1014
- assert_matches %Query{
1015
- metrics: [:visitors],
1016
- utc_time_range: ^@date_range_day,
1017
- filters: [],
1018
- dimensions: ["time"],
1019
- order_by: nil,
1020
- timezone: ^site.timezone,
1021
- include: ^@default_include,
1022
- pagination: %{limit: 100, offset: 200}
1023
- } = query
1024
- end
1025
-
1026
- test "out of range limit value", %{site: site} do
1027
- params = %{
1028
- "site_id" => site.domain,
1029
- "metrics" => ["visitors"],
1030
- "date_range" => "all",
1031
- "pagination" => %{"limit" => 100_000}
1032
- }
1033
-
1034
- assert {:error, %QueryError{message: error}} =
1035
- Query.parse_and_build(site, params, now: @now)
1036
-
1037
- assert error == "#/pagination/limit: Expected the value to be <= 10000"
1038
- end
1039
-
1040
- test "out of range offset value", %{site: site} do
1041
- params = %{
1042
- "site_id" => site.domain,
1043
- "metrics" => ["visitors"],
1044
- "date_range" => "all",
1045
- "pagination" => %{"offset" => -5}
1046
- }
1047
-
1048
- assert {:error, %QueryError{message: error}} =
1049
- Query.parse_and_build(site, params, now: @now)
1050
-
1051
- assert error == "#/pagination/offset: Expected the value to be >= 0"
1052
- end
1053
- end
1054
-
1055
- describe "event:goal filter validation" do
1056
- test "valid filters", %{site: site} do
1057
- insert(:goal, %{site: site, event_name: "Signup"})
1058
- insert(:goal, %{site: site, page_path: "/thank-you"})
1059
-
1060
- params = %{
1061
- "site_id" => site.domain,
1062
- "metrics" => ["visitors"],
1063
- "date_range" => "all",
1064
- "filters" => [
1065
- ["is", "event:goal", ["Signup", "Visit /thank-you"]]
1066
- ]
1067
- }
1068
-
1069
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1070
-
1071
- assert_matches %Query{
1072
- metrics: [:visitors],
1073
- utc_time_range: ^@date_range_day,
1074
- filters: [
1075
- [:is, "event:goal", ["Signup", "Visit /thank-you"]]
1076
- ],
1077
- dimensions: [],
1078
- order_by: nil,
1079
- timezone: ^site.timezone,
1080
- include: ^@default_include,
1081
- pagination: %{limit: 10_000, offset: 0}
1082
- } = query
1083
-
1084
- check_goals(query,
1085
- preloaded_goals: %{
1086
- all: ["Signup", "Visit /thank-you"],
1087
- matching_toplevel_filters: ["Signup", "Visit /thank-you"]
1088
- },
1089
- revenue_warning: nil,
1090
- revenue_currencies: %{}
1091
- )
1092
- end
1093
-
1094
- test "invalid event filter", %{site: site} do
1095
- params = %{
1096
- "site_id" => site.domain,
1097
- "metrics" => ["visitors"],
1098
- "date_range" => "all",
1099
- "filters" => [
1100
- ["is", "event:goal", ["Signup"]]
1101
- ]
1102
- }
1103
-
1104
- assert {:error, %QueryError{message: error}} =
1105
- Query.parse_and_build(site, params, now: @now)
1106
-
1107
- assert error ==
1108
- "Invalid filters. The goal `Signup` is not configured for this site. Find out how to configure goals here: https://plausible.io/docs/stats-api#filtering-by-goals"
1109
- end
1110
-
1111
- test "invalid page filter", %{site: site} do
1112
- params = %{
1113
- "site_id" => site.domain,
1114
- "metrics" => ["visitors"],
1115
- "date_range" => "all",
1116
- "filters" => [
1117
- ["is", "event:goal", ["Visit /thank-you"]]
1118
- ]
1119
- }
1120
-
1121
- assert {:error, %QueryError{message: error}} =
1122
- Query.parse_and_build(site, params, now: @now)
1123
-
1124
- assert error ==
1125
- "Invalid filters. The goal `Visit /thank-you` is not configured for this site. Find out how to configure goals here: https://plausible.io/docs/stats-api#filtering-by-goals"
1126
- end
1127
-
1128
- test "unsupported filter", %{site: site} do
1129
- params = %{
1130
- "site_id" => site.domain,
1131
- "metrics" => ["visitors"],
1132
- "date_range" => "all",
1133
- "filters" => [
1134
- ["is_not", "event:goal", ["Signup"]]
1135
- ]
1136
- }
1137
-
1138
- assert {:error, %QueryError{message: error}} =
1139
- Query.parse_and_build(site, params, now: @now)
1140
-
1141
- assert error == "#/filters/0: Invalid filter [\"is_not\", \"event:goal\", [\"Signup\"]]"
1142
- end
1143
-
1144
- test "not top-level filter", %{site: site} do
1145
- params = %{
1146
- "site_id" => site.domain,
1147
- "metrics" => ["visitors"],
1148
- "date_range" => "all",
1149
- "filters" => [
1150
- [
1151
- "or",
1152
- [
1153
- ["is", "event:goal", ["Signup"]],
1154
- ["is", "event:name", ["pageview"]]
1155
- ]
1156
- ]
1157
- ]
1158
- }
1159
-
1160
- assert {:error, %QueryError{message: error}} =
1161
- Query.parse_and_build(site, params, now: @now)
1162
-
1163
- assert error ==
1164
- "Invalid filters. Dimension `event:goal` can only be filtered at the top level."
1165
- end
1166
-
1167
- test "allowed within behavioral filters has_done", %{site: site} do
1168
- insert(:goal, %{site: site, event_name: "Signup"})
1169
-
1170
- params = %{
1171
- "site_id" => site.domain,
1172
- "metrics" => ["visitors"],
1173
- "date_range" => "all",
1174
- "filters" => [
1175
- [
1176
- "has_done",
1177
- [
1178
- "or",
1179
- [
1180
- ["is", "event:goal", ["Signup"]],
1181
- ["is", "event:name", ["pageview"]]
1182
- ]
1183
- ]
1184
- ]
1185
- ]
1186
- }
1187
-
1188
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1189
-
1190
- assert_matches %Query{
1191
- metrics: [:visitors],
1192
- utc_time_range: ^@date_range_day,
1193
- filters: [
1194
- [
1195
- :has_done,
1196
- [
1197
- :or,
1198
- [
1199
- [:is, "event:goal", ["Signup"]],
1200
- [:is, "event:name", ["pageview"]]
1201
- ]
1202
- ]
1203
- ]
1204
- ],
1205
- dimensions: [],
1206
- order_by: nil,
1207
- timezone: ^site.timezone,
1208
- include: ^@default_include,
1209
- pagination: %{limit: 10_000, offset: 0}
1210
- } = query
1211
-
1212
- check_goals(query,
1213
- preloaded_goals: %{all: ["Signup"], matching_toplevel_filters: ["Signup"]},
1214
- revenue_warning: nil,
1215
- revenue_currencies: %{}
1216
- )
1217
- end
1218
- end
1219
-
1220
- describe "date range validation" do
1221
- for shortcut <- ["realtime", "30m"] do
1222
- test "public API does not recognize #{shortcut} date_range", %{site: site} do
1223
- params = %{
1224
- "site_id" => site.domain,
1225
- "metrics" => ["visitors"],
1226
- "date_range" => unquote(shortcut)
1227
- }
1228
-
1229
- assert {:error, %QueryError{message: error}} =
1230
- Query.parse_and_build(site, params, now: @now)
1231
-
1232
- assert error =~ "Invalid date range"
1233
- end
1234
- end
1235
-
1236
- for {shortcut, expected_date_range} <- [
1237
- {"day", @date_range_today},
1238
- {"7d", @date_range_7d},
1239
- {"10d", @date_range_10d},
1240
- {"30d", @date_range_30d},
1241
- {"month", @date_range_month},
1242
- {"3mo", @date_range_3mo},
1243
- {"6mo", @date_range_6mo},
1244
- {"12mo", @date_range_12mo},
1245
- {"year", @date_range_year}
1246
- ] do
1247
- test "parses '#{shortcut}' date_range shortcut ", %{site: site} do
1248
- params = %{
1249
- "site_id" => site.domain,
1250
- "metrics" => ["visitors", "events"],
1251
- "date_range" => unquote(shortcut)
1252
- }
1253
-
1254
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1255
-
1256
- assert_matches %Query{
1257
- metrics: [:visitors, :events],
1258
- utc_time_range: ^unquote(Macro.escape(expected_date_range)),
1259
- filters: [],
1260
- dimensions: [],
1261
- order_by: nil,
1262
- timezone: ^site.timezone,
1263
- include: ^@default_include,
1264
- pagination: %{limit: 10_000, offset: 0}
1265
- } = query
1266
- end
1267
- end
1268
-
1269
- test "parsing `all` with previous data", %{site: site} do
1270
- site = Map.put(site, :stats_start_date, ~D[2020-01-01])
1271
- expected_date_range = DateTimeRange.new!(~D[2020-01-01], ~D[2021-05-05], "Etc/UTC")
1272
-
1273
- params = %{
1274
- "site_id" => site.domain,
1275
- "metrics" => ["visitors", "events"],
1276
- "date_range" => "all"
1277
- }
1278
-
1279
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1280
-
1281
- assert_matches %Query{
1282
- metrics: [:visitors, :events],
1283
- utc_time_range: ^expected_date_range,
1284
- filters: [],
1285
- dimensions: [],
1286
- order_by: nil,
1287
- timezone: ^site.timezone,
1288
- include: ^@default_include,
1289
- pagination: %{limit: 10_000, offset: 0}
1290
- } = query
1291
- end
1292
-
1293
- test "parsing `all` with no previous data", %{site: site} do
1294
- site = Map.put(site, :stats_start_date, nil)
1295
-
1296
- params = %{
1297
- "site_id" => site.domain,
1298
- "metrics" => ["visitors", "events"],
1299
- "date_range" => "all"
1300
- }
1301
-
1302
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1303
-
1304
- assert_matches %Query{
1305
- metrics: [:visitors, :events],
1306
- utc_time_range: ^@date_range_day,
1307
- filters: [],
1308
- dimensions: [],
1309
- order_by: nil,
1310
- timezone: ^site.timezone,
1311
- include: ^@default_include,
1312
- pagination: %{limit: 10_000, offset: 0}
1313
- } = query
1314
- end
1315
-
1316
- test "parsing custom date range from simple date strings", %{site: site} do
1317
- params = %{
1318
- "site_id" => site.domain,
1319
- "metrics" => ["visitors", "events"],
1320
- "date_range" => ["2021-05-05", "2021-05-05"]
1321
- }
1322
-
1323
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1324
-
1325
- assert_matches %Query{
1326
- metrics: [:visitors, :events],
1327
- utc_time_range: ^@date_range_day,
1328
- filters: [],
1329
- dimensions: [],
1330
- order_by: nil,
1331
- timezone: ^site.timezone,
1332
- include: ^@default_include,
1333
- pagination: %{limit: 10_000, offset: 0}
1334
- } = query
1335
- end
1336
-
1337
- test "parsing custom date range from iso8601 timestamps", %{site: site} do
1338
- params = %{
1339
- "site_id" => site.domain,
1340
- "metrics" => ["visitors", "events"],
1341
- "date_range" => ["2024-01-01T00:00:00Z", "2024-01-02T23:59:59Z"]
1342
- }
1343
-
1344
- expected_utc_time_range =
1345
- DateTimeRange.new!(
1346
- DateTime.new!(~D[2024-01-01], ~T[00:00:00], "Etc/UTC"),
1347
- DateTime.new!(~D[2024-01-02], ~T[23:59:59], "Etc/UTC")
1348
- )
1349
-
1350
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1351
-
1352
- assert_matches %Query{
1353
- metrics: [:visitors, :events],
1354
- utc_time_range: ^expected_utc_time_range,
1355
- filters: [],
1356
- dimensions: [],
1357
- order_by: nil,
1358
- timezone: ^site.timezone,
1359
- include: ^@default_include,
1360
- pagination: %{limit: 10_000, offset: 0}
1361
- } = query
1362
- end
1363
-
1364
- test "parsing custom date range from iso8601 timestamps with non-UTC timezone", %{site: site} do
1365
- params = %{
1366
- "site_id" => site.domain,
1367
- "metrics" => ["visitors", "events"],
1368
- "date_range" => ["2024-08-29T07:12:34-07:00", "2024-08-29T10:12:34-07:00"]
1369
- }
1370
-
1371
- expected_utc_time_range =
1372
- DateTimeRange.new!(~U[2024-08-29 14:12:34Z], ~U[2024-08-29 17:12:34Z])
1373
-
1374
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1375
-
1376
- assert_matches %Query{
1377
- metrics: [:visitors, :events],
1378
- utc_time_range: ^expected_utc_time_range,
1379
- filters: [],
1380
- dimensions: [],
1381
- order_by: nil,
1382
- timezone: ^site.timezone,
1383
- include: ^@default_include,
1384
- pagination: %{limit: 10_000, offset: 0}
1385
- } = query
1386
- end
1387
-
1388
- for invalid_value <- ["-1d", "foo", ["21415-00", "eee"]] do
1389
- test "errors on invalid date range value (#{inspect(invalid_value)})", %{site: site} do
1390
- params = %{
1391
- "site_id" => site.domain,
1392
- "date_range" => unquote(invalid_value),
1393
- "metrics" => ["visitors"]
1394
- }
1395
-
1396
- assert {:error, %QueryError{message: error}} =
1397
- Query.parse_and_build(site, params, now: @now)
1398
-
1399
- assert error == "#/date_range: Invalid date range #{inspect(unquote(invalid_value))}"
1400
- end
1401
- end
1402
-
1403
- test "999999999mo is invalid date range", %{site: site} do
1404
- params = %{
1405
- "site_id" => site.domain,
1406
- "date_range" => "999999999mo",
1407
- "metrics" => ["visitors"]
1408
- }
1409
-
1410
- assert {:error, %QueryError{message: error}} =
1411
- Query.parse_and_build(site, params, now: @now)
1412
-
1413
- assert error == "Invalid date_range \"999999999mo\""
1414
- end
1415
-
1416
- test "custom date range is invalid when timestamps do not include timezone info", %{
1417
- site: site
1418
- } do
1419
- params = %{
1420
- "site_id" => site.domain,
1421
- "date_range" => ["2021-02-03T00:00:00", "2021-02-03T23:59:59"],
1422
- "metrics" => ["visitors"]
1423
- }
1424
-
1425
- assert {:error, %QueryError{message: error}} =
1426
- Query.parse_and_build(site, params, now: @now)
1427
-
1428
- assert error == "Invalid date_range '[\"2021-02-03T00:00:00\", \"2021-02-03T23:59:59\"]'."
1429
- end
1430
-
1431
- test "custom date range is invalid when timestamp timezone is invalid", %{site: site} do
1432
- params = %{
1433
- "site_id" => site.domain,
1434
- "date_range" => ["2021-02-03T00:00:00-25:00", "2021-02-03T23:59:59-25:00"],
1435
- "metrics" => ["visitors"]
1436
- }
1437
-
1438
- assert {:error, %QueryError{message: error}} =
1439
- Query.parse_and_build(site, params, now: @now)
1440
-
1441
- assert error ==
1442
- "#/date_range: Invalid date range [\"2021-02-03T00:00:00-25:00\", \"2021-02-03T23:59:59-25:00\"]"
1443
- end
1444
-
1445
- test "custom date range is invalid when date and timestamp are combined", %{site: site} do
1446
- params = %{
1447
- "site_id" => site.domain,
1448
- "date_range" => ["2021-02-03T00:00:00Z", "2021-02-04"],
1449
- "metrics" => ["visitors"]
1450
- }
1451
-
1452
- assert {:error, %QueryError{message: error}} =
1453
- Query.parse_and_build(site, params, now: @now)
1454
-
1455
- assert error ==
1456
- "#/date_range: Invalid date range [\"2021-02-03T00:00:00Z\", \"2021-02-04\"]"
1457
- end
1458
-
1459
- test "parses date_range.first into a datetime right after the gap in site.timezone", %{
1460
- site: site
1461
- } do
1462
- site = %{site | timezone: "America/Santiago"}
1463
-
1464
- params = %{
1465
- "site_id" => site.domain,
1466
- "metrics" => ["visitors"],
1467
- "date_range" => ["2022-09-11", "2022-09-11"]
1468
- }
1469
-
1470
- expected_utc_time_range =
1471
- DateTimeRange.new!(~U[2022-09-11 04:00:00Z], ~U[2022-09-12 02:59:59Z])
1472
-
1473
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1474
-
1475
- assert_matches %Query{
1476
- metrics: [:visitors],
1477
- utc_time_range: ^expected_utc_time_range,
1478
- filters: [],
1479
- dimensions: [],
1480
- order_by: nil,
1481
- timezone: ^site.timezone,
1482
- include: ^@default_include,
1483
- pagination: %{limit: 10_000, offset: 0}
1484
- } = query
1485
- end
1486
-
1487
- test "parses date_range.first into the latest of ambiguous datetimes in site.timezone", %{
1488
- site: site
1489
- } do
1490
- site = %{site | timezone: "America/Havana"}
1491
-
1492
- params = %{
1493
- "site_id" => site.domain,
1494
- "metrics" => ["visitors"],
1495
- "date_range" => ["2023-11-05", "2023-11-05"]
1496
- }
1497
-
1498
- expected_utc_time_range =
1499
- DateTimeRange.new!(~U[2023-11-05 05:00:00Z], ~U[2023-11-06 04:59:59Z])
1500
-
1501
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1502
-
1503
- assert_matches %Query{
1504
- metrics: [:visitors],
1505
- utc_time_range: ^expected_utc_time_range,
1506
- filters: [],
1507
- dimensions: [],
1508
- order_by: nil,
1509
- timezone: ^site.timezone,
1510
- include: ^@default_include,
1511
- pagination: %{limit: 10_000, offset: 0}
1512
- } = query
1513
- end
1514
-
1515
- test "parses date_range.last into the earliest of ambiguous datetimes in site.timezone", %{
1516
- site: site
1517
- } do
1518
- site = %{site | timezone: "America/Asuncion"}
1519
-
1520
- params = %{
1521
- "site_id" => site.domain,
1522
- "metrics" => ["visitors"],
1523
- "date_range" => ["2024-03-23", "2024-03-23"]
1524
- }
1525
-
1526
- expected_utc_time_range =
1527
- DateTimeRange.new!(~U[2024-03-23 03:00:00Z], ~U[2024-03-24 02:59:59Z])
1528
-
1529
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1530
-
1531
- assert_matches %Query{
1532
- metrics: [:visitors],
1533
- utc_time_range: ^expected_utc_time_range,
1534
- filters: [],
1535
- dimensions: [],
1536
- order_by: nil,
1537
- timezone: ^site.timezone,
1538
- include: ^@default_include,
1539
- pagination: %{limit: 10_000, offset: 0}
1540
- } = query
1541
- end
1542
- end
1543
-
1544
- describe "dimensions validation" do
1545
- for dimension <- Filters.event_props() do
1546
- test "event:#{dimension} dimension", %{site: site} do
1547
- prefixed_dimension = "event:#{unquote(dimension)}"
1548
-
1549
- params = %{
1550
- "site_id" => site.domain,
1551
- "metrics" => ["visitors"],
1552
- "date_range" => "all",
1553
- "dimensions" => [prefixed_dimension]
1554
- }
1555
-
1556
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1557
-
1558
- assert_matches %Query{
1559
- metrics: [:visitors],
1560
- utc_time_range: ^@date_range_day,
1561
- filters: [],
1562
- dimensions: [^prefixed_dimension],
1563
- order_by: nil,
1564
- timezone: ^site.timezone,
1565
- include: ^@default_include,
1566
- pagination: %{limit: 10_000, offset: 0}
1567
- } = query
1568
- end
1569
- end
1570
-
1571
- for dimension <- Filters.visit_props() do
1572
- test "visit:#{dimension} dimension", %{site: site} do
1573
- prefixed_dimension = "visit:#{unquote(dimension)}"
1574
-
1575
- params = %{
1576
- "site_id" => site.domain,
1577
- "metrics" => ["visitors"],
1578
- "date_range" => "all",
1579
- "dimensions" => [prefixed_dimension]
1580
- }
1581
-
1582
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1583
-
1584
- assert_matches %Query{
1585
- metrics: [:visitors],
1586
- utc_time_range: ^@date_range_day,
1587
- filters: [],
1588
- dimensions: [^prefixed_dimension],
1589
- order_by: nil,
1590
- timezone: ^site.timezone,
1591
- include: ^@default_include,
1592
- pagination: %{limit: 10_000, offset: 0}
1593
- } = query
1594
- end
1595
- end
1596
-
1597
- test "time:minute dimension fails public schema validation", %{site: site} do
1598
- params = %{
1599
- "site_id" => site.domain,
1600
- "metrics" => ["visitors"],
1601
- "date_range" => "all",
1602
- "dimensions" => ["time:minute"]
1603
- }
1604
-
1605
- assert {:error, %QueryError{message: error}} =
1606
- Query.parse_and_build(site, params, now: @now)
1607
-
1608
- assert error == "#/dimensions/0: Invalid dimension \"time:minute\""
1609
- end
1610
-
1611
- test "custom properties dimension", %{site: site} do
1612
- params = %{
1613
- "site_id" => site.domain,
1614
- "metrics" => ["visitors"],
1615
- "date_range" => "all",
1616
- "dimensions" => ["event:props:foobar"]
1617
- }
1618
-
1619
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1620
-
1621
- assert_matches %Query{
1622
- metrics: [:visitors],
1623
- utc_time_range: ^@date_range_day,
1624
- filters: [],
1625
- dimensions: ["event:props:foobar"],
1626
- order_by: nil,
1627
- timezone: ^site.timezone,
1628
- include: ^@default_include,
1629
- pagination: %{limit: 10_000, offset: 0}
1630
- } = query
1631
- end
1632
-
1633
- test "invalid custom property dimension", %{site: site} do
1634
- params = %{
1635
- "site_id" => site.domain,
1636
- "metrics" => ["visitors"],
1637
- "date_range" => "all",
1638
- "dimensions" => ["event:props:"]
1639
- }
1640
-
1641
- assert {:error, %QueryError{message: error}} =
1642
- Query.parse_and_build(site, params, now: @now)
1643
-
1644
- assert error == "#/dimensions/0: Invalid dimension \"event:props:\""
1645
- end
1646
-
1647
- test "invalid dimension name passed", %{site: site} do
1648
- params = %{
1649
- "site_id" => site.domain,
1650
- "metrics" => ["visitors"],
1651
- "date_range" => "all",
1652
- "dimensions" => ["visitors"]
1653
- }
1654
-
1655
- assert {:error, %QueryError{message: error}} =
1656
- Query.parse_and_build(site, params, now: @now)
1657
-
1658
- assert error == "#/dimensions/0: Invalid dimension \"visitors\""
1659
- end
1660
-
1661
- test "invalid dimension", %{site: site} do
1662
- params = %{
1663
- "site_id" => site.domain,
1664
- "metrics" => ["visitors"],
1665
- "date_range" => "all",
1666
- "dimensions" => "foobar"
1667
- }
1668
-
1669
- assert {:error, %QueryError{message: error}} =
1670
- Query.parse_and_build(site, params, now: @now)
1671
-
1672
- assert error == "#/dimensions: Type mismatch. Expected Array but got String."
1673
- end
1674
-
1675
- test "dimensions are not unique", %{site: site} do
1676
- params = %{
1677
- "site_id" => site.domain,
1678
- "metrics" => ["visitors"],
1679
- "date_range" => "all",
1680
- "dimensions" => ["event:name", "event:name"]
1681
- }
1682
-
1683
- assert {:error, %QueryError{message: error}} =
1684
- Query.parse_and_build(site, params, now: @now)
1685
-
1686
- assert error == "#/dimensions: Expected items to be unique but they were not."
1687
- end
1688
- end
1689
-
1690
- describe "order_by validation" do
1691
- test "ordering by metric", %{site: site} do
1692
- params = %{
1693
- "site_id" => site.domain,
1694
- "metrics" => ["visitors", "events"],
1695
- "date_range" => "all",
1696
- "order_by" => [["events", "desc"], ["visitors", "asc"]]
1697
- }
1698
-
1699
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1700
-
1701
- assert_matches %Query{
1702
- metrics: [:visitors, :events],
1703
- utc_time_range: ^@date_range_day,
1704
- filters: [],
1705
- dimensions: [],
1706
- order_by: [{:events, :desc}, {:visitors, :asc}],
1707
- timezone: ^site.timezone,
1708
- include: ^@default_include,
1709
- pagination: %{limit: 10_000, offset: 0}
1710
- } = query
1711
- end
1712
-
1713
- test "ordering by dimension", %{site: site} do
1714
- params = %{
1715
- "site_id" => site.domain,
1716
- "metrics" => ["visitors"],
1717
- "date_range" => "all",
1718
- "dimensions" => ["event:name"],
1719
- "order_by" => [["event:name", "desc"]]
1720
- }
1721
-
1722
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1723
-
1724
- assert_matches %Query{
1725
- metrics: [:visitors],
1726
- utc_time_range: ^@date_range_day,
1727
- filters: [],
1728
- dimensions: ["event:name"],
1729
- order_by: [{"event:name", :desc}],
1730
- timezone: ^site.timezone,
1731
- include: ^@default_include,
1732
- pagination: %{limit: 10_000, offset: 0}
1733
- } = query
1734
- end
1735
-
1736
- test "ordering by invalid value", %{site: site} do
1737
- params = %{
1738
- "site_id" => site.domain,
1739
- "metrics" => ["visitors"],
1740
- "date_range" => "all",
1741
- "order_by" => [["visssss", "desc"]]
1742
- }
1743
-
1744
- assert {:error, %QueryError{message: error}} =
1745
- Query.parse_and_build(site, params, now: @now)
1746
-
1747
- assert error == "#/order_by/0/0: Invalid value in order_by \"visssss\""
1748
- end
1749
-
1750
- test "ordering by not queried metric", %{site: site} do
1751
- params = %{
1752
- "site_id" => site.domain,
1753
- "metrics" => ["visitors"],
1754
- "date_range" => "all",
1755
- "order_by" => [["events", "desc"]]
1756
- }
1757
-
1758
- assert {:error, %QueryError{message: error}} =
1759
- Query.parse_and_build(site, params, now: @now)
1760
-
1761
- assert error ==
1762
- "Invalid order_by entry '{:events, :desc}'. Entry is not a queried metric or dimension."
1763
- end
1764
-
1765
- test "ordering by not queried dimension", %{site: site} do
1766
- params = %{
1767
- "site_id" => site.domain,
1768
- "metrics" => ["visitors"],
1769
- "date_range" => "all",
1770
- "order_by" => [["event:name", "desc"]]
1771
- }
1772
-
1773
- assert {:error, %QueryError{message: error}} =
1774
- Query.parse_and_build(site, params, now: @now)
1775
-
1776
- assert error ==
1777
- "Invalid order_by entry '{\"event:name\", :desc}'. Entry is not a queried metric or dimension."
1778
- end
1779
- end
1780
-
1781
- describe "custom props access" do
1782
- test "filters - no access", %{site: site, user: user} do
1783
- subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.StatsAPI])
1784
-
1785
- params = %{
1786
- "site_id" => site.domain,
1787
- "metrics" => ["visitors"],
1788
- "date_range" => "all",
1789
- "filters" => [["not", ["is", "event:props:foobar", ["foo"]]]]
1790
- }
1791
-
1792
- assert {:error, %QueryError{message: error}} =
1793
- Query.parse_and_build(site, params, now: @now)
1794
-
1795
- assert error ==
1796
- "The owner of this site does not have access to the custom properties feature."
1797
- end
1798
-
1799
- test "dimensions - no access", %{site: site, user: user} do
1800
- subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.StatsAPI])
1801
-
1802
- params = %{
1803
- "site_id" => site.domain,
1804
- "metrics" => ["visitors"],
1805
- "date_range" => "all",
1806
- "dimensions" => ["event:props:foobar"]
1807
- }
1808
-
1809
- assert {:error, %QueryError{message: error}} =
1810
- Query.parse_and_build(site, params, now: @now)
1811
-
1812
- assert error ==
1813
- "The owner of this site does not have access to the custom properties feature."
1814
- end
1815
- end
1816
-
1817
- describe "conversion_rate metric" do
1818
- test "fails validation on its own", %{site: site} do
1819
- params = %{
1820
- "site_id" => site.domain,
1821
- "metrics" => ["conversion_rate"],
1822
- "date_range" => "all"
1823
- }
1824
-
1825
- assert {:error, %QueryError{message: error}} =
1826
- Query.parse_and_build(site, params, now: @now)
1827
-
1828
- assert error ==
1829
- "Metric `conversion_rate` can only be queried with event:goal filters or dimensions."
1830
- end
1831
-
1832
- test "succeeds with event:goal filter", %{site: site} do
1833
- insert(:goal, %{site: site, event_name: "Signup"})
1834
- insert(:goal, %{site: site, event_name: "Purchase", currency: "USD"})
1835
-
1836
- params = %{
1837
- "site_id" => site.domain,
1838
- "metrics" => ["conversion_rate"],
1839
- "date_range" => "all",
1840
- "filters" => [["is", "event:goal", ["Signup"]]]
1841
- }
1842
-
1843
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1844
-
1845
- assert_matches %Query{
1846
- metrics: [:conversion_rate],
1847
- utc_time_range: ^@date_range_day,
1848
- filters: [[:is, "event:goal", ["Signup"]]],
1849
- dimensions: [],
1850
- order_by: nil,
1851
- timezone: ^site.timezone,
1852
- include: ^@default_include,
1853
- pagination: %{limit: 10_000, offset: 0}
1854
- } = query
1855
-
1856
- check_goals(query,
1857
- preloaded_goals: %{
1858
- all: ["Purchase", "Signup"],
1859
- matching_toplevel_filters: ["Signup"]
1860
- },
1861
- revenue_currencies: %{}
1862
- )
1863
- end
1864
-
1865
- test "succeeds with event:goal dimension", %{site: site} do
1866
- insert(:goal, %{site: site, event_name: "Purchase", currency: "USD"})
1867
- insert(:goal, %{site: site, event_name: "Signup"})
1868
-
1869
- params = %{
1870
- "site_id" => site.domain,
1871
- "metrics" => ["conversion_rate"],
1872
- "date_range" => "all",
1873
- "dimensions" => ["event:goal"]
1874
- }
1875
-
1876
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1877
-
1878
- assert_matches %Query{
1879
- metrics: [:conversion_rate],
1880
- utc_time_range: ^@date_range_day,
1881
- filters: [],
1882
- dimensions: ["event:goal"],
1883
- order_by: nil,
1884
- timezone: ^site.timezone,
1885
- include: ^@default_include,
1886
- pagination: %{limit: 10_000, offset: 0}
1887
- } = query
1888
-
1889
- check_goals(query,
1890
- preloaded_goals: %{
1891
- all: ["Purchase", "Signup"],
1892
- matching_toplevel_filters: ["Purchase", "Signup"]
1893
- },
1894
- revenue_currencies: %{}
1895
- )
1896
- end
1897
-
1898
- test "custom properties filter with special metric", %{site: site} do
1899
- params = %{
1900
- "site_id" => site.domain,
1901
- "metrics" => ["conversion_rate", "group_conversion_rate"],
1902
- "date_range" => "all",
1903
- "filters" => [["is", "event:props:foo", ["bar"]]],
1904
- "dimensions" => ["event:goal"]
1905
- }
1906
-
1907
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1908
-
1909
- assert_matches %Query{
1910
- metrics: [:conversion_rate, :group_conversion_rate],
1911
- utc_time_range: ^@date_range_day,
1912
- filters: [
1913
- [:is, "event:props:foo", ["bar"]]
1914
- ],
1915
- dimensions: ["event:goal"],
1916
- order_by: nil,
1917
- timezone: ^site.timezone,
1918
- include: ^@default_include,
1919
- pagination: %{limit: 10_000, offset: 0}
1920
- } = query
1921
- end
1922
-
1923
- test "not top level custom properties filter with special metric is invalid", %{site: site} do
1924
- params = %{
1925
- "site_id" => site.domain,
1926
- "metrics" => ["conversion_rate", "group_conversion_rate"],
1927
- "date_range" => "all",
1928
- "filters" => [["not", ["is", "event:props:foo", ["bar"]]]],
1929
- "dimensions" => ["event:goal"]
1930
- }
1931
-
1932
- assert {:error, %QueryError{message: error}} =
1933
- Query.parse_and_build(site, params, now: @now)
1934
-
1935
- assert error ==
1936
- "Invalid filters. When `conversion_rate` or `group_conversion_rate` metrics are used, custom property filters can only be used on top level."
1937
- end
1938
- end
1939
-
1940
- describe "scroll_depth metric" do
1941
- test "fails validation on its own", %{site: site} do
1942
- params = %{
1943
- "site_id" => site.domain,
1944
- "metrics" => ["scroll_depth"],
1945
- "date_range" => "all"
1946
- }
1947
-
1948
- assert {:error, %QueryError{message: error}} =
1949
- Query.parse_and_build(site, params, now: @now)
1950
-
1951
- assert error ==
1952
- "Metric `scroll_depth` can only be queried with event:page filters or dimensions."
1953
- end
1954
-
1955
- test "fails with only a non-top-level event:page filter", %{site: site} do
1956
- params = %{
1957
- "site_id" => site.domain,
1958
- "metrics" => ["scroll_depth"],
1959
- "date_range" => "all",
1960
- "filters" => [["not", ["is", "event:page", ["/"]]]]
1961
- }
1962
-
1963
- assert {:error, %QueryError{message: error}} =
1964
- Query.parse_and_build(site, params, now: @now)
1965
-
1966
- assert error ==
1967
- "Metric `scroll_depth` can only be queried with event:page filters or dimensions."
1968
- end
1969
-
1970
- test "succeeds with top-level event:page filter", %{site: site} do
1971
- params = %{
1972
- "site_id" => site.domain,
1973
- "metrics" => ["scroll_depth"],
1974
- "date_range" => "all",
1975
- "filters" => [["is", "event:page", ["/"]]]
1976
- }
1977
-
1978
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
1979
-
1980
- assert_matches %Query{
1981
- metrics: [:scroll_depth],
1982
- utc_time_range: ^@date_range_day,
1983
- filters: [[:is, "event:page", ["/"]]],
1984
- dimensions: [],
1985
- order_by: nil,
1986
- timezone: ^site.timezone,
1987
- include: ^@default_include,
1988
- pagination: %{limit: 10_000, offset: 0}
1989
- } = query
1990
- end
1991
-
1992
- test "succeeds with event:page dimension", %{site: site} do
1993
- params = %{
1994
- "site_id" => site.domain,
1995
- "metrics" => ["scroll_depth"],
1996
- "date_range" => "all",
1997
- "dimensions" => ["event:page"]
1998
- }
1999
-
2000
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2001
-
2002
- assert_matches %Query{
2003
- metrics: [:scroll_depth],
2004
- utc_time_range: ^@date_range_day,
2005
- filters: [],
2006
- dimensions: ["event:page"],
2007
- order_by: nil,
2008
- timezone: ^site.timezone,
2009
- include: ^@default_include,
2010
- pagination: %{limit: 10_000, offset: 0}
2011
- } = query
2012
- end
2013
- end
2014
-
2015
- describe "views_per_visit metric" do
2016
- test "succeeds with normal filters", %{site: site} do
2017
- insert(:goal, %{site: site, event_name: "Signup"})
2018
-
2019
- params = %{
2020
- "site_id" => site.domain,
2021
- "metrics" => ["views_per_visit"],
2022
- "date_range" => "all",
2023
- "filters" => [["is", "event:goal", ["Signup"]]]
2024
- }
2025
-
2026
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2027
-
2028
- assert_matches %Query{
2029
- metrics: [:views_per_visit],
2030
- utc_time_range: ^@date_range_day,
2031
- filters: [[:is, "event:goal", ["Signup"]]],
2032
- dimensions: [],
2033
- order_by: nil,
2034
- timezone: ^site.timezone,
2035
- include: ^@default_include,
2036
- pagination: %{limit: 10_000, offset: 0}
2037
- } = query
2038
-
2039
- check_goals(query,
2040
- preloaded_goals: %{all: ["Signup"], matching_toplevel_filters: ["Signup"]},
2041
- revenue_currencies: %{}
2042
- )
2043
- end
2044
-
2045
- test "fails validation if event:page filter specified", %{site: site} do
2046
- params = %{
2047
- "site_id" => site.domain,
2048
- "metrics" => ["views_per_visit"],
2049
- "date_range" => "all",
2050
- "filters" => [["is", "event:page", ["/"]]]
2051
- }
2052
-
2053
- assert {:error, %QueryError{message: error}} =
2054
- Query.parse_and_build(site, params, now: @now)
2055
-
2056
- assert error == "Metric `views_per_visit` cannot be queried with a filter on `event:page`."
2057
- end
2058
-
2059
- test "fails validation with non-time dimensions", %{site: site} do
2060
- params = %{
2061
- "site_id" => site.domain,
2062
- "metrics" => ["views_per_visit"],
2063
- "date_range" => "all",
2064
- "dimensions" => ["event:name"]
2065
- }
2066
-
2067
- assert {:error, %QueryError{message: error}} =
2068
- Query.parse_and_build(site, params, now: @now)
2069
-
2070
- assert error == "Metric `views_per_visit` cannot be queried with non-time dimensions."
2071
- end
2072
- end
2073
-
2074
- describe "time_on_page metric" do
2075
- test "fails validation on its own", %{site: site} do
2076
- params = %{
2077
- "site_id" => site.domain,
2078
- "metrics" => ["time_on_page"],
2079
- "date_range" => "all"
2080
- }
2081
-
2082
- assert {:error, %QueryError{message: error}} =
2083
- Query.parse_and_build(site, params, now: @now)
2084
-
2085
- assert error ==
2086
- "Metric `time_on_page` can only be queried with event:page filters or dimensions."
2087
- end
2088
-
2089
- test "succeeds with event:page dimension", %{site: site} do
2090
- params = %{
2091
- "site_id" => site.domain,
2092
- "metrics" => ["time_on_page"],
2093
- "date_range" => "all",
2094
- "dimensions" => ["time", "event:page"]
2095
- }
2096
-
2097
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2098
-
2099
- assert_matches %Query{
2100
- metrics: [:time_on_page],
2101
- utc_time_range: ^@date_range_day,
2102
- filters: [],
2103
- dimensions: ["time", "event:page"],
2104
- order_by: nil,
2105
- timezone: ^site.timezone,
2106
- include: ^@default_include,
2107
- pagination: %{limit: 10_000, offset: 0}
2108
- } = query
2109
- end
2110
-
2111
- test "succeeds with event:page filter", %{site: site} do
2112
- params = %{
2113
- "site_id" => site.domain,
2114
- "metrics" => ["time_on_page"],
2115
- "date_range" => "all",
2116
- "filters" => [["is", "event:page", ["/"]]]
2117
- }
2118
-
2119
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2120
-
2121
- assert_matches %Query{
2122
- metrics: [:time_on_page],
2123
- utc_time_range: ^@date_range_day,
2124
- filters: [[:is, "event:page", ["/"]]],
2125
- dimensions: [],
2126
- order_by: nil,
2127
- timezone: ^site.timezone,
2128
- include: ^@default_include,
2129
- pagination: %{limit: 10_000, offset: 0}
2130
- } = query
2131
- end
2132
-
2133
- test "fails when using only a behavioral filter", %{site: site} do
2134
- params = %{
2135
- "site_id" => site.domain,
2136
- "metrics" => ["time_on_page"],
2137
- "date_range" => "all",
2138
- "filters" => [
2139
- ["has_done", ["is", "event:page", ["/"]]]
2140
- ]
2141
- }
2142
-
2143
- assert {:error, %QueryError{message: error}} =
2144
- Query.parse_and_build(site, params, now: @now)
2145
-
2146
- assert error ==
2147
- "Metric `time_on_page` can only be queried with event:page filters or dimensions."
2148
- end
2149
- end
2150
-
2151
- describe "revenue metrics" do
2152
- @describetag :ee_only
2153
-
2154
- setup %{user: user} do
2155
- subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.RevenueGoals])
2156
- :ok
2157
- end
2158
-
2159
- test "can request", %{site: site} do
2160
- params = %{
2161
- "site_id" => site.domain,
2162
- "metrics" => ["total_revenue", "average_revenue"],
2163
- "date_range" => "all"
2164
- }
2165
-
2166
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2167
-
2168
- assert_matches %Query{
2169
- metrics: [:total_revenue, :average_revenue],
2170
- utc_time_range: ^@date_range_day,
2171
- filters: [],
2172
- dimensions: [],
2173
- order_by: nil,
2174
- timezone: ^site.timezone,
2175
- include: ^@default_include,
2176
- pagination: %{limit: 10_000, offset: 0}
2177
- } = query
2178
-
2179
- check_goals(query,
2180
- preloaded_goals: %{
2181
- all: [],
2182
- matching_toplevel_filters: []
2183
- },
2184
- revenue_warning: :no_revenue_goals_matching,
2185
- revenue_currencies: %{}
2186
- )
2187
- end
2188
-
2189
- test "no access" do
2190
- user = new_user()
2191
- site = new_site(owner: user)
2192
-
2193
- subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.StatsAPI])
2194
-
2195
- params = %{
2196
- "site_id" => site.domain,
2197
- "metrics" => ["total_revenue", "average_revenue"],
2198
- "date_range" => "all"
2199
- }
2200
-
2201
- assert {:error, %QueryError{message: error}} =
2202
- Query.parse_and_build(site, params, now: @now)
2203
-
2204
- assert error ==
2205
- "The owner of this site does not have access to the revenue metrics feature."
2206
- end
2207
-
2208
- test "with event:goal filters with same currency", %{site: site} do
2209
- insert(:goal,
2210
- site: site,
2211
- event_name: "Purchase",
2212
- currency: "USD",
2213
- display_name: "PurchaseUSD"
2214
- )
2215
-
2216
- insert(:goal, site: site, event_name: "Subscription", currency: "USD")
2217
- insert(:goal, site: site, event_name: "Signup")
2218
- insert(:goal, site: site, event_name: "Logout")
2219
-
2220
- params = %{
2221
- "site_id" => site.domain,
2222
- "metrics" => ["total_revenue", "average_revenue"],
2223
- "date_range" => "all",
2224
- "filters" => [["is", "event:goal", ["PurchaseUSD", "Signup", "Subscription"]]]
2225
- }
2226
-
2227
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2228
-
2229
- assert_matches %Query{
2230
- metrics: [:total_revenue, :average_revenue],
2231
- utc_time_range: ^@date_range_day,
2232
- filters: [[:is, "event:goal", ["PurchaseUSD", "Signup", "Subscription"]]],
2233
- dimensions: [],
2234
- order_by: nil,
2235
- timezone: ^site.timezone,
2236
- include: ^@default_include,
2237
- pagination: %{limit: 10_000, offset: 0}
2238
- } = query
2239
-
2240
- check_goals(query,
2241
- preloaded_goals: %{
2242
- all: ["PurchaseUSD", "Signup", "Subscription", "Logout"],
2243
- matching_toplevel_filters: ["PurchaseUSD", "Signup", "Subscription"]
2244
- },
2245
- revenue_warning: nil,
2246
- revenue_currencies: %{default: :USD}
2247
- )
2248
- end
2249
-
2250
- test "with event:goal filters with different currencies", %{site: site} do
2251
- insert(:goal, site: site, event_name: "Purchase", currency: "USD")
2252
- insert(:goal, site: site, event_name: "Subscription", currency: "EUR")
2253
- insert(:goal, site: site, event_name: "Signup")
2254
-
2255
- params = %{
2256
- "site_id" => site.domain,
2257
- "metrics" => ["total_revenue", "average_revenue"],
2258
- "date_range" => "all",
2259
- "filters" => [["is", "event:goal", ["Purchase", "Signup", "Subscription"]]]
2260
- }
2261
-
2262
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2263
-
2264
- assert_matches %Query{
2265
- metrics: [:total_revenue, :average_revenue],
2266
- utc_time_range: ^@date_range_day,
2267
- filters: [[:is, "event:goal", ["Purchase", "Signup", "Subscription"]]],
2268
- dimensions: [],
2269
- order_by: nil,
2270
- timezone: ^site.timezone,
2271
- include: ^@default_include,
2272
- pagination: %{limit: 10_000, offset: 0}
2273
- } = query
2274
-
2275
- check_goals(query,
2276
- preloaded_goals: %{
2277
- all: ["Purchase", "Signup", "Subscription"],
2278
- matching_toplevel_filters: ["Purchase", "Signup", "Subscription"]
2279
- },
2280
- revenue_warning: :no_single_revenue_currency,
2281
- revenue_currencies: %{}
2282
- )
2283
- end
2284
-
2285
- test "with event:goal filters with no revenue currencies", %{site: site} do
2286
- insert(:goal, site: site, event_name: "Purchase", currency: "USD")
2287
- insert(:goal, site: site, event_name: "Subscription", currency: "EUR")
2288
- insert(:goal, site: site, event_name: "Signup")
2289
-
2290
- params = %{
2291
- "site_id" => site.domain,
2292
- "metrics" => ["total_revenue", "average_revenue"],
2293
- "date_range" => "all",
2294
- "filters" => [["is", "event:goal", ["Signup"]]]
2295
- }
2296
-
2297
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2298
-
2299
- assert_matches %Query{
2300
- metrics: [:total_revenue, :average_revenue],
2301
- utc_time_range: ^@date_range_day,
2302
- filters: [[:is, "event:goal", ["Signup"]]],
2303
- dimensions: [],
2304
- order_by: nil,
2305
- timezone: ^site.timezone,
2306
- include: ^@default_include,
2307
- pagination: %{limit: 10_000, offset: 0}
2308
- } = query
2309
-
2310
- check_goals(query,
2311
- preloaded_goals: %{
2312
- all: ["Purchase", "Subscription", "Signup"],
2313
- matching_toplevel_filters: ["Signup"]
2314
- },
2315
- revenue_warning: :no_revenue_goals_matching,
2316
- revenue_currencies: %{}
2317
- )
2318
- end
2319
-
2320
- test "with event:goal dimension, different currencies", %{site: site} do
2321
- insert(:goal, site: site, event_name: "Purchase", currency: "USD")
2322
- insert(:goal, site: site, event_name: "Donation", currency: "EUR")
2323
- insert(:goal, site: site, event_name: "Signup")
2324
-
2325
- params = %{
2326
- "site_id" => site.domain,
2327
- "metrics" => ["total_revenue", "average_revenue"],
2328
- "date_range" => "all",
2329
- "dimensions" => ["event:goal"]
2330
- }
2331
-
2332
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2333
-
2334
- assert_matches %Query{
2335
- metrics: [:total_revenue, :average_revenue],
2336
- utc_time_range: ^@date_range_day,
2337
- filters: [],
2338
- dimensions: ["event:goal"],
2339
- order_by: nil,
2340
- timezone: ^site.timezone,
2341
- include: ^@default_include,
2342
- pagination: %{limit: 10_000, offset: 0}
2343
- } = query
2344
-
2345
- check_goals(query,
2346
- preloaded_goals: %{
2347
- all: ["Donation", "Purchase", "Signup"],
2348
- matching_toplevel_filters: ["Donation", "Purchase", "Signup"]
2349
- },
2350
- revenue_warning: nil,
2351
- revenue_currencies: %{"Donation" => :EUR, "Purchase" => :USD}
2352
- )
2353
- end
2354
-
2355
- test "with event:goal dimension and filters", %{site: site} do
2356
- insert(:goal, site: site, event_name: "Purchase", currency: "USD")
2357
- insert(:goal, site: site, event_name: "Subscription", currency: "EUR")
2358
- insert(:goal, site: site, event_name: "Signup")
2359
- insert(:goal, site: site, event_name: "Logout")
2360
-
2361
- params = %{
2362
- "site_id" => site.domain,
2363
- "metrics" => ["total_revenue", "average_revenue"],
2364
- "date_range" => "all",
2365
- "dimensions" => ["event:goal"],
2366
- "filters" => [["is", "event:goal", ["Purchase", "Signup", "Subscription"]]]
2367
- }
2368
-
2369
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2370
-
2371
- assert_matches %Query{
2372
- metrics: [:total_revenue, :average_revenue],
2373
- utc_time_range: ^@date_range_day,
2374
- filters: [[:is, "event:goal", ["Purchase", "Signup", "Subscription"]]],
2375
- dimensions: ["event:goal"],
2376
- order_by: nil,
2377
- timezone: ^site.timezone,
2378
- include: ^@default_include,
2379
- pagination: %{limit: 10_000, offset: 0}
2380
- } = query
2381
-
2382
- check_goals(query,
2383
- preloaded_goals: %{
2384
- all: ["Logout", "Purchase", "Signup", "Subscription"],
2385
- matching_toplevel_filters: ["Purchase", "Signup", "Subscription"]
2386
- },
2387
- revenue_warning: nil,
2388
- revenue_currencies: %{"Purchase" => :USD, "Subscription" => :EUR}
2389
- )
2390
- end
2391
-
2392
- test "with event:goal dimension and filters with no revenue goals matching", %{
2393
- site: site
2394
- } do
2395
- insert(:goal, site: site, event_name: "Purchase", currency: "USD")
2396
- insert(:goal, site: site, event_name: "Subscription", currency: "USD")
2397
- insert(:goal, site: site, event_name: "Signup")
2398
- insert(:goal, site: site, event_name: "Logout")
2399
-
2400
- params = %{
2401
- "site_id" => site.domain,
2402
- "metrics" => ["total_revenue", "average_revenue"],
2403
- "date_range" => "all",
2404
- "dimensions" => ["event:goal"],
2405
- "filters" => [["is", "event:goal", ["Signup"]]]
2406
- }
2407
-
2408
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2409
-
2410
- assert_matches %Query{
2411
- metrics: [:total_revenue, :average_revenue],
2412
- utc_time_range: ^@date_range_day,
2413
- filters: [[:is, "event:goal", ["Signup"]]],
2414
- dimensions: ["event:goal"],
2415
- order_by: nil,
2416
- timezone: ^site.timezone,
2417
- include: ^@default_include,
2418
- pagination: %{limit: 10_000, offset: 0}
2419
- } = query
2420
-
2421
- check_goals(query,
2422
- preloaded_goals: %{
2423
- all: ["Logout", "Signup", "Subscription", "Purchase"],
2424
- matching_toplevel_filters: ["Signup"]
2425
- },
2426
- revenue_warning: :no_revenue_goals_matching,
2427
- revenue_currencies: %{}
2428
- )
2429
- end
2430
- end
2431
-
2432
- @tag :ce_build_only
2433
- test "revenue metrics are not available on CE", %{site: site} do
2434
- params = %{
2435
- "site_id" => site.domain,
2436
- "metrics" => ["total_revenue", "average_revenue"],
2437
- "date_range" => "all"
2438
- }
2439
-
2440
- assert {:error, %QueryError{message: error}} =
2441
- Query.parse_and_build(site, params, now: @now)
2442
-
2443
- assert error ==
2444
- "#/metrics/0: Invalid metric \"total_revenue\"\n#/metrics/1: Invalid metric \"average_revenue\""
2445
- end
2446
-
2447
- describe "session metrics" do
2448
- test "single session metric succeeds", %{site: site} do
2449
- params = %{
2450
- "site_id" => site.domain,
2451
- "metrics" => ["bounce_rate"],
2452
- "date_range" => "all",
2453
- "dimensions" => ["visit:device"]
2454
- }
2455
-
2456
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2457
-
2458
- assert_matches %Query{
2459
- metrics: [:bounce_rate],
2460
- utc_time_range: ^@date_range_day,
2461
- filters: [],
2462
- dimensions: ["visit:device"],
2463
- order_by: nil,
2464
- timezone: ^site.timezone,
2465
- include: ^@default_include,
2466
- pagination: %{limit: 10_000, offset: 0}
2467
- } = query
2468
- end
2469
-
2470
- test "fails if using session metric with event dimension", %{site: site} do
2471
- params = %{
2472
- "site_id" => site.domain,
2473
- "metrics" => ["bounce_rate"],
2474
- "date_range" => "all",
2475
- "dimensions" => ["event:props:foo"]
2476
- }
2477
-
2478
- assert {:error, %QueryError{message: error}} =
2479
- Query.parse_and_build(site, params, now: @now)
2480
-
2481
- assert error ==
2482
- "Session metric(s) `bounce_rate` cannot be queried along with event dimension(s) `event:props:foo`"
2483
- end
2484
-
2485
- test "fails if using event metric with session-only dimension", %{site: site} do
2486
- params = %{
2487
- "site_id" => site.domain,
2488
- "metrics" => ["events"],
2489
- "date_range" => "all",
2490
- "dimensions" => ["visit:exit_page"]
2491
- }
2492
-
2493
- assert {:error, %QueryError{message: error}} =
2494
- Query.parse_and_build(site, params, now: @now)
2495
-
2496
- assert error ==
2497
- "Event metric(s) `events` cannot be queried along with session dimension(s) `visit:exit_page`"
2498
- end
2499
-
2500
- test "does not fail if using session metric with event:page dimension", %{site: site} do
2501
- params = %{
2502
- "site_id" => site.domain,
2503
- "metrics" => ["bounce_rate"],
2504
- "date_range" => "all",
2505
- "dimensions" => ["event:page"]
2506
- }
2507
-
2508
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2509
-
2510
- assert_matches %Query{
2511
- metrics: [:bounce_rate],
2512
- utc_time_range: ^@date_range_day,
2513
- filters: [],
2514
- dimensions: ["event:page"],
2515
- order_by: nil,
2516
- timezone: ^site.timezone,
2517
- include: ^@default_include,
2518
- pagination: %{limit: 10_000, offset: 0}
2519
- } = query
2520
- end
2521
-
2522
- test "does not fail if using session metric with event filter", %{site: site} do
2523
- params = %{
2524
- "site_id" => site.domain,
2525
- "metrics" => ["bounce_rate"],
2526
- "date_range" => "all",
2527
- "filters" => [["is", "event:props:foo", ["(none)"]]]
2528
- }
2529
-
2530
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2531
-
2532
- assert_matches %Query{
2533
- metrics: [:bounce_rate],
2534
- utc_time_range: ^@date_range_day,
2535
- filters: [[:is, "event:props:foo", ["(none)"]]],
2536
- dimensions: [],
2537
- order_by: nil,
2538
- timezone: ^site.timezone,
2539
- include: ^@default_include,
2540
- pagination: %{limit: 10_000, offset: 0}
2541
- } = query
2542
- end
2543
- end
2544
-
2545
- describe "filtering with segments" do
2546
- test "parsing fails when too many segments in query", %{
2547
- user: user,
2548
- site: site
2549
- } do
2550
- segments =
2551
- insert_list(11, :segment,
2552
- type: :site,
2553
- owner: user,
2554
- site: site,
2555
- name: "any"
2556
- )
2557
-
2558
- params = %{
2559
- "site_id" => site.domain,
2560
- "metrics" => ["visitors"],
2561
- "date_range" => "all",
2562
- "filters" => [
2563
- ["and", segments |> Enum.map(fn segment -> ["is", "segment", [segment.id]] end)]
2564
- ]
2565
- }
2566
-
2567
- assert {:error, %QueryError{message: error}} =
2568
- Query.parse_and_build(site, params, now: @now)
2569
-
2570
- assert error == "Invalid filters. You can only use up to 10 segment filters in a query."
2571
- end
2572
-
2573
- test "parsing fails when segment filter is used, but segment is from another site", %{
2574
- site: site
2575
- } do
2576
- other_user = new_user()
2577
- other_site = new_site(owner: other_user)
2578
-
2579
- segment =
2580
- insert(:segment,
2581
- type: :site,
2582
- owner: other_user,
2583
- site: other_site,
2584
- name: "any"
2585
- )
2586
-
2587
- params = %{
2588
- "site_id" => site.domain,
2589
- "metrics" => ["visitors"],
2590
- "date_range" => "all",
2591
- "filters" => [["is", "segment", [segment.id]]]
2592
- }
2593
-
2594
- assert {:error, %QueryError{message: error}} =
2595
- Query.parse_and_build(site, params, now: @now)
2596
-
2597
- assert error == "Invalid filters. Some segments don't exist or aren't accessible."
2598
- end
2599
-
2600
- test "hiding custom properties filters in segments doesn't allow bypasssing feature check",
2601
- %{
2602
- site: site,
2603
- user: user
2604
- } do
2605
- subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.StatsAPI])
2606
-
2607
- segment =
2608
- insert(:segment,
2609
- type: :site,
2610
- owner: user,
2611
- site: site,
2612
- name: "segment with custom props filter",
2613
- segment_data: %{"filters" => [["is", "event:props:foobar", ["foo"]]]}
2614
- )
2615
-
2616
- params = %{
2617
- "site_id" => site.domain,
2618
- "metrics" => ["visitors"],
2619
- "date_range" => "all",
2620
- "filters" => [["is", "segment", [segment.id]]]
2621
- }
2622
-
2623
- assert {:error, %QueryError{message: error}} =
2624
- Query.parse_and_build(site, params, now: @now)
2625
-
2626
- assert error ==
2627
- "The owner of this site does not have access to the custom properties feature."
2628
- end
2629
-
2630
- test "querying conversion rate is illegal if the complex event:goal filter is within a segment",
2631
- %{
2632
- site: site,
2633
- user: user
2634
- } do
2635
- segment =
2636
- insert(:segment,
2637
- type: :site,
2638
- owner: user,
2639
- site: site,
2640
- name: "any",
2641
- segment_data: %{
2642
- "filters" => [
2643
- [
2644
- "or",
2645
- [
2646
- ["is", "event:goal", ["Signup"]],
2647
- ["contains", "event:page", ["/"]]
2648
- ]
2649
- ]
2650
- ]
2651
- }
2652
- )
2653
-
2654
- params = %{
2655
- "site_id" => site.domain,
2656
- "metrics" => ["visitors", "conversion_rate"],
2657
- "date_range" => "all",
2658
- "filters" => [["is", "segment", [segment.id]]]
2659
- }
2660
-
2661
- assert {:error, %QueryError{message: error}} =
2662
- Query.parse_and_build(site, params, now: @now)
2663
-
2664
- assert error ==
2665
- "Invalid filters. Dimension `event:goal` can only be filtered at the top level."
2666
- end
2667
-
2668
- test "resolves segments correctly", %{site: site, user: user} do
2669
- emea_segment =
2670
- insert(:segment,
2671
- type: :site,
2672
- owner: user,
2673
- site: site,
2674
- name: "EMEA",
2675
- segment_data: %{
2676
- "filters" => [["is", "visit:country", ["FR", "DE"]]],
2677
- "labels" => %{"FR" => "France", "DE" => "Germany"}
2678
- }
2679
- )
2680
-
2681
- apac_segment =
2682
- insert(:segment,
2683
- type: :site,
2684
- owner: user,
2685
- site: site,
2686
- name: "APAC",
2687
- segment_data: %{
2688
- "filters" => [["is", "visit:country", ["AU", "NZ"]]],
2689
- "labels" => %{"AU" => "Australia", "NZ" => "New Zealand"}
2690
- }
2691
- )
2692
-
2693
- firefox_segment =
2694
- insert(:segment,
2695
- type: :site,
2696
- owner: user,
2697
- site: site,
2698
- name: "APAC",
2699
- segment_data: %{
2700
- "filters" => [
2701
- ["is", "visit:browser", ["Firefox"]],
2702
- ["is", "visit:os", ["Linux"]]
2703
- ]
2704
- }
2705
- )
2706
-
2707
- params = %{
2708
- "site_id" => site.domain,
2709
- "metrics" => ["visitors", "events"],
2710
- "date_range" => "all",
2711
- "filters" => [
2712
- [
2713
- "and",
2714
- [
2715
- ["is", "segment", [apac_segment.id, emea_segment.id]],
2716
- ["is", "segment", [firefox_segment.id]]
2717
- ]
2718
- ]
2719
- ]
2720
- }
2721
-
2722
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2723
-
2724
- assert_matches %Query{
2725
- metrics: [:visitors, :events],
2726
- utc_time_range: ^@date_range_day,
2727
- filters: [
2728
- [
2729
- :or,
2730
- [
2731
- [:and, [[:is, "visit:country", ["AU", "NZ"]]]],
2732
- [:and, [[:is, "visit:country", ["FR", "DE"]]]]
2733
- ]
2734
- ],
2735
- [
2736
- :and,
2737
- [
2738
- [:is, "visit:browser", ["Firefox"]],
2739
- [:is, "visit:os", ["Linux"]]
2740
- ]
2741
- ]
2742
- ],
2743
- dimensions: [],
2744
- order_by: nil,
2745
- timezone: ^site.timezone,
2746
- include: ^@default_include,
2747
- pagination: %{limit: 10_000, offset: 0}
2748
- } = query
2749
- end
2750
-
2751
- test "resolves segments containing otherwise internal features", %{site: site, user: user} do
2752
- insert(:goal, %{site: site, event_name: "Signup"})
2753
-
2754
- segment_from_dashboard =
2755
- insert(:segment,
2756
- name: "A segment that contains internal features",
2757
- type: :site,
2758
- owner: user,
2759
- site: site,
2760
- segment_data: %{
2761
- "filters" => [["matches_wildcard", "event:page", ["/blog/**"]]]
2762
- }
2763
- )
2764
-
2765
- params = %{
2766
- "site_id" => site.domain,
2767
- "metrics" => ["visitors", "events"],
2768
- "date_range" => "all",
2769
- "filters" => [
2770
- ["is", "segment", [segment_from_dashboard.id]]
2771
- ]
2772
- }
2773
-
2774
- assert {:ok, query} = Query.parse_and_build(site, params, now: @now)
2775
-
2776
- assert_matches %Query{
2777
- metrics: [:visitors, :events],
2778
- utc_time_range: ^@date_range_day,
2779
- filters: [
2780
- [:matches_wildcard, "event:page", ["/blog/**"]]
2781
- ],
2782
- dimensions: [],
2783
- order_by: nil,
2784
- timezone: ^site.timezone,
2785
- include: ^@default_include,
2786
- pagination: %{limit: 10_000, offset: 0}
2787
- } = query
2788
- end
2789
-
2790
- test "validation fails with string segment ids", %{site: site} do
2791
- params = %{
2792
- "site_id" => site.domain,
2793
- "metrics" => ["visitors"],
2794
- "date_range" => "all",
2795
- "filters" => [["is", "segment", ["123"]]]
2796
- }
2797
-
2798
- assert {:error, %QueryError{message: error}} =
2799
- Query.parse_and_build(site, params, now: @now)
2800
-
2801
- assert error == "Invalid filter '[\"is\", \"segment\", [\"123\"]]'."
2802
- end
2803
- end
2804
- end