Skip to content

Commit 2694302

Browse files
authored
Scroll depth dashboard warnings (imported data) (#5051)
* add migration * add schema field * mark site_imports with has_scroll_depth * add function to get imports in query range * add scroll_depth metric warning in QueryResult * return scroll_depth warning in top stats * render minimalistic warning in top stats * minimalistic warning in Top Pages breakdown * prettier format * silence credo * add test * use a snapshot of SiteImport schema in data migration * also use a snapshot list of imported_* tables * moduledoc (credo) * change tooltip message * change metric warnings structure in top stats response * pass meta from queryresult directly * revert top_stats_entry refactor * prettier * stop using SiteImport module in data migration
1 parent 9f54e1f commit 2694302

File tree

17 files changed

+499
-41
lines changed

17 files changed

+499
-41
lines changed

assets/js/dashboard/components/sort-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ export const SortButton = ({
1717
return (
1818
<button
1919
onClick={toggleSort}
20-
title={next.hint}
2120
className={classNames('group', 'hover:underline', 'relative')}
2221
>
2322
{children}
2423
<span
24+
title={next.hint}
2525
className={classNames(
2626
'absolute',
2727
'rounded inline-block h-4 w-4',

assets/js/dashboard/components/table.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import classNames from 'classnames'
44
import React, { ReactNode } from 'react'
55
import { SortDirection } from '../hooks/use-order-by'
66
import { SortButton } from './sort-button'
7+
import { Tooltip } from '../util/tooltip'
78

89
export type ColumnConfiguraton<T extends Record<string, unknown>> = {
910
/** Unique column ID, used for sorting purposes and to get the value of the cell using rowItem[key] */
@@ -17,6 +18,8 @@ export type ColumnConfiguraton<T extends Record<string, unknown>> = {
1718
width: string
1819
/** Aligns column content. */
1920
align?: 'left' | 'right'
21+
/** A warning to be rendered as a tooltip for the column header */
22+
metricWarning?: string
2023
/**
2124
* Function used to transform the value found at item[key] for the cell. Superseded by renderItem if present. @example 1120 => "1.1k"
2225
*/
@@ -100,6 +103,29 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
100103
columns: ColumnConfiguraton<T>[]
101104
data: T[] | { pages: T[][] }
102105
}) => {
106+
const renderColumnLabel = (column: ColumnConfiguraton<T>) => {
107+
if (column.metricWarning) {
108+
return (
109+
<Tooltip
110+
info={warningSpan(column.metricWarning)}
111+
className="inline-block"
112+
>
113+
{column.label + ' *'}
114+
</Tooltip>
115+
)
116+
} else {
117+
return column.label
118+
}
119+
}
120+
121+
const warningSpan = (warning: string) => {
122+
return (
123+
<span className="text-xs font-normal whitespace-nowrap">
124+
{'*' + warning}
125+
</span>
126+
)
127+
}
128+
103129
return (
104130
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
105131
<thead>
@@ -115,10 +141,10 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
115141
toggleSort={column.onSort}
116142
sortDirection={column.sortDirection ?? null}
117143
>
118-
{column.label}
144+
{renderColumnLabel(column)}
119145
</SortButton>
120146
) : (
121-
column.label
147+
renderColumnLabel(column)
122148
)}
123149
</TableHeaderCell>
124150
))}

assets/js/dashboard/query.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export type DashboardQuery = typeof queryDefaultValue
5252
export type BreakdownResultMeta = {
5353
date_range_label: string
5454
comparison_date_range_label?: string
55+
metric_warnings: Record<string, Record<string, string>> | undefined
5556
}
5657

5758
export function addFilter(

assets/js/dashboard/stats/graph/top-stats.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ export default function TopStats({ data, onMetricUpdate, tooltipBoundary }) {
6464
<SecondsSinceLastLoad lastLoadTimestamp={lastLoadTimestamp} />s ago
6565
</p>
6666
)}
67+
68+
{stat.name === 'Scroll depth' &&
69+
data.meta.metric_warnings?.scroll_depth?.code ===
70+
'no_imported_scroll_depth' && (
71+
<p className="font-normal text-xs whitespace-nowrap">
72+
* Does not include imported data
73+
</p>
74+
)}
6775
</div>
6876
)
6977
}
@@ -115,6 +123,7 @@ export default function TopStats({ data, onMetricUpdate, tooltipBoundary }) {
115123
{statExtraName && (
116124
<span className="hidden sm:inline-block ml-1">{statExtraName}</span>
117125
)}
126+
{stat.warning_code && <span className="inline-block ml-1">*</span>}
118127
</div>
119128
)
120129
}

assets/js/dashboard/stats/modals/breakdown-modal.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export default function BreakdownModal<TListItem extends { name: string }>({
152152
key: m.key,
153153
width: m.width,
154154
align: 'right',
155+
metricWarning: getMetricWarning(m, meta),
155156
renderValue: (item) => m.renderValue(item, meta),
156157
onSort: m.sortable ? () => toggleSortByMetric(m) : undefined,
157158
sortDirection: orderByDictionary[m.key]
@@ -233,3 +234,15 @@ const ExternalLinkIcon = ({ url }: { url?: string }) =>
233234
</svg>
234235
</a>
235236
) : null
237+
238+
const getMetricWarning = (metric: Metric, meta: BreakdownResultMeta | null) => {
239+
const warnings = meta?.metric_warnings
240+
241+
if (warnings) {
242+
const code = warnings[metric.key]?.code
243+
244+
if (code == 'no_imported_scroll_depth') {
245+
return 'Does not include imported data'
246+
}
247+
}
248+
}

lib/plausible/data_migration/site_imports.ex

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,51 @@ defmodule Plausible.DataMigration.SiteImports do
99

1010
import Ecto.Query
1111

12-
alias Plausible.ClickhouseRepo
13-
alias Plausible.Imported
14-
alias Plausible.Imported.SiteImport
15-
alias Plausible.Repo
16-
alias Plausible.Site
12+
alias Plausible.{Repo, ClickhouseRepo, Site}
1713

18-
require Plausible.Imported.SiteImport
14+
defmodule SiteImportSnapshot do
15+
@moduledoc """
16+
A snapshot of the Plausible.Imported.SiteImport schema from April 2024.
17+
"""
18+
19+
use Ecto.Schema
20+
21+
schema "site_imports" do
22+
field :start_date, :date
23+
field :end_date, :date
24+
field :label, :string
25+
field :source, Ecto.Enum, values: [:universal_analytics, :google_analytics_4, :csv, :noop]
26+
field :status, Ecto.Enum, values: [:pending, :importing, :completed, :failed]
27+
field :legacy, :boolean, default: false
28+
29+
belongs_to :site, Plausible.Site
30+
belongs_to :imported_by, Plausible.Auth.User
31+
32+
timestamps()
33+
end
34+
end
35+
36+
@imported_tables_april_2024 [
37+
"imported_visitors",
38+
"imported_sources",
39+
"imported_pages",
40+
"imported_entry_pages",
41+
"imported_exit_pages",
42+
"imported_custom_events",
43+
"imported_locations",
44+
"imported_devices",
45+
"imported_browsers",
46+
"imported_operating_systems"
47+
]
48+
49+
def imported_tables_april_2024(), do: @imported_tables_april_2024
1950

2051
def run(opts \\ []) do
2152
dry_run? = Keyword.get(opts, :dry_run?, true)
2253

2354
site_import_query =
24-
from(i in Imported.SiteImport,
25-
where: i.site_id == parent_as(:site).id and i.status == ^SiteImport.completed(),
55+
from(i in SiteImportSnapshot,
56+
where: i.site_id == parent_as(:site).id and i.status == ^:completed,
2657
select: 1
2758
)
2859

@@ -37,7 +68,7 @@ defmodule Plausible.DataMigration.SiteImports do
3768
|> Repo.all(log: false)
3869

3970
site_imports =
40-
from(i in Imported.SiteImport, where: i.status == ^SiteImport.completed())
71+
from(i in SiteImportSnapshot, where: i.status == ^:completed)
4172
|> Repo.all(log: false)
4273

4374
legacy_site_imports = backfill_legacy_site_imports(sites_with_only_legacy_import, dry_run?)
@@ -64,7 +95,7 @@ defmodule Plausible.DataMigration.SiteImports do
6495
|> Map.put(:site_id, site.id)
6596
|> Map.take([:legacy, :start_date, :end_date, :source, :status, :site_id])
6697

67-
%Imported.SiteImport{}
98+
%SiteImportSnapshot{}
6899
|> Ecto.Changeset.change(params)
69100
|> insert!(dry_run?)
70101
end
@@ -146,11 +177,11 @@ defmodule Plausible.DataMigration.SiteImports do
146177
# Exposed for testing purposes
147178
@doc false
148179
def imported_stats_end_date(site_id, import_ids) do
149-
[first_schema | schemas] = Imported.schemas()
180+
[first_table | tables] = @imported_tables_april_2024
150181

151182
query =
152-
Enum.reduce(schemas, max_date_query(first_schema, site_id, import_ids), fn schema, query ->
153-
from(s in subquery(union_all(query, ^max_date_query(schema, site_id, import_ids))))
183+
Enum.reduce(tables, max_date_query(first_table, site_id, import_ids), fn table, query ->
184+
from(s in subquery(union_all(query, ^max_date_query(table, site_id, import_ids))))
154185
end)
155186

156187
dates = ClickhouseRepo.all(from(q in query, select: q.max_date), log: false)
@@ -211,8 +242,8 @@ defmodule Plausible.DataMigration.SiteImports do
211242
entity
212243
end
213244

214-
defp max_date_query(schema, site_id, import_ids) do
215-
from(q in schema,
245+
defp max_date_query(table, site_id, import_ids) do
246+
from(q in table,
216247
where: q.site_id == ^site_id,
217248
where: q.import_id in ^import_ids,
218249
select: %{max_date: fragment("max(?)", q.date)}
@@ -222,12 +253,12 @@ defmodule Plausible.DataMigration.SiteImports do
222253
defp from_legacy(%Site.ImportedData{} = data) do
223254
status =
224255
case data.status do
225-
"ok" -> SiteImport.completed()
226-
"error" -> SiteImport.failed()
227-
_ -> SiteImport.importing()
256+
"ok" -> :completed
257+
"error" -> :failed
258+
_ -> :importing
228259
end
229260

230-
%SiteImport{
261+
%SiteImportSnapshot{
231262
id: 0,
232263
legacy: true,
233264
start_date: data.start_date,

lib/plausible/imported.ex

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ defmodule Plausible.Imported do
99

1010
import Ecto.Query
1111

12-
alias Plausible.Imported
12+
alias Plausible.{Site, Repo, Imported}
1313
alias Plausible.Imported.SiteImport
14-
alias Plausible.Repo
15-
alias Plausible.Site
14+
alias Plausible.Stats.Query
1615

1716
require Plausible.Imported.SiteImport
1817

@@ -97,6 +96,17 @@ defmodule Plausible.Imported do
9796
end
9897
end
9998

99+
@spec completed_imports_in_query_range(Site.t(), Query.t()) :: [SiteImport.t()]
100+
def completed_imports_in_query_range(%Site{} = site, %Query{} = query) do
101+
date_range = Query.date_range(query)
102+
103+
site
104+
|> get_completed_imports()
105+
|> Enum.filter(fn site_import ->
106+
site_import.start_date <= date_range.last && site_import.end_date >= date_range.first
107+
end)
108+
end
109+
100110
@spec get_import(Site.t(), non_neg_integer()) :: SiteImport.t() | nil
101111
def get_import(site, import_id) do
102112
Repo.get_by(SiteImport, id: import_id, site_id: site.id)

lib/plausible/imported/csv_importer.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule Plausible.Imported.CSVImporter do
55
"""
66

77
use Plausible.Imported.Importer
8+
import Ecto.Query, only: [from: 2]
89

910
@impl true
1011
def name(), do: :csv
@@ -54,6 +55,26 @@ defmodule Plausible.Imported.CSVImporter do
5455
{:error, Exception.message(e)}
5556
end
5657

58+
def on_success(site_import, _extra_data) do
59+
has_scroll_depth? =
60+
Plausible.ClickhouseRepo.exists?(
61+
from(i in "imported_pages",
62+
where: i.site_id == ^site_import.site_id,
63+
where: i.import_id == ^site_import.id,
64+
where: not is_nil(i.scroll_depth),
65+
select: 1
66+
)
67+
)
68+
69+
if has_scroll_depth? do
70+
site_import
71+
|> Ecto.Changeset.change(%{has_scroll_depth: true})
72+
|> Plausible.Repo.update!()
73+
end
74+
75+
:ok
76+
end
77+
5778
defp import_s3(ch, site_import, uploads) do
5879
%{
5980
id: import_id,

lib/plausible/imported/site_import.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule Plausible.Imported.SiteImport do
2222
field :source, Ecto.Enum, values: ImportSources.names()
2323
field :status, Ecto.Enum, values: @statuses
2424
field :legacy, :boolean, default: false
25+
field :has_scroll_depth, :boolean, default: false
2526

2627
belongs_to :site, Site
2728
belongs_to :imported_by, User

lib/plausible/stats/query_result.ex

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Plausible.Stats.QueryResult do
99

1010
use Plausible
1111
alias Plausible.Stats.{DateTimeRange, Query, QueryRunner}
12+
alias Plausible.Imported
1213

1314
defstruct results: [],
1415
meta: %{},
@@ -89,8 +90,8 @@ defmodule Plausible.Stats.QueryResult do
8990
end
9091
end
9192

92-
defp add_metric_warnings_meta(meta, %QueryRunner{main_query: query}) do
93-
warnings = metric_warnings(query)
93+
defp add_metric_warnings_meta(meta, runner) do
94+
warnings = metric_warnings(meta, runner)
9495

9596
if map_size(warnings) > 0 do
9697
Map.put(meta, :metric_warnings, warnings)
@@ -129,9 +130,9 @@ defmodule Plausible.Stats.QueryResult do
129130
end
130131
end
131132

132-
defp metric_warnings(query) do
133+
defp metric_warnings(meta, %QueryRunner{main_query: query} = runner) do
133134
Enum.reduce(query.metrics, %{}, fn metric, acc ->
134-
case metric_warning(metric, query) do
135+
case metric_warning(metric, meta, runner) do
135136
nil -> acc
136137
%{} = warning -> Map.put(acc, metric, warning)
137138
end
@@ -150,7 +151,8 @@ defmodule Plausible.Stats.QueryResult do
150151
"Revenue metrics are null as there are no matching revenue goals."
151152
}
152153

153-
defp metric_warning(metric, query) when metric in @revenue_metrics do
154+
defp metric_warning(metric, _meta, %QueryRunner{main_query: query})
155+
when metric in @revenue_metrics do
154156
if query.revenue_warning do
155157
%{
156158
code: query.revenue_warning,
@@ -162,7 +164,30 @@ defmodule Plausible.Stats.QueryResult do
162164
end
163165
end
164166

165-
defp metric_warning(_metric, _query), do: nil
167+
@no_imported_scroll_depth_metric_warning %{
168+
code: :no_imported_scroll_depth,
169+
warning: "No imports with scroll depth data were found"
170+
}
171+
172+
defp metric_warning(:scroll_depth, %{imports_included: true} = _meta, %QueryRunner{} = runner) do
173+
%QueryRunner{site: site, main_query: main_query, comparison_query: comparison_query} = runner
174+
175+
imports_in_main_range = Imported.completed_imports_in_query_range(site, main_query)
176+
177+
imports_in_comparison_range =
178+
case comparison_query do
179+
nil -> []
180+
%Query{} = query -> Imported.completed_imports_in_query_range(site, query)
181+
end
182+
183+
imports_in_range = imports_in_main_range ++ imports_in_comparison_range
184+
185+
if not Enum.any?(imports_in_range, & &1.has_scroll_depth) do
186+
@no_imported_scroll_depth_metric_warning
187+
end
188+
end
189+
190+
defp metric_warning(_metric, _meta, _query), do: nil
166191

167192
defp to_iso8601(datetime, timezone) do
168193
datetime

0 commit comments

Comments
 (0)