Skip to content

Commit 0bb5be7

Browse files
committed
chg(coat): user statisitcs data from plausible
1 parent 995d2c8 commit 0bb5be7

3 files changed

Lines changed: 146 additions & 0 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,8 @@ SERVICES_ORG=ninanor
8989
SERVICES_STORAGE_PREFIX=/dms/tables
9090
GIT_USERNAME=
9191
GIT_TOKEN=
92+
93+
# COAT
94+
COAT_API_KEY=
95+
COAT_BASE_URL=
96+
COAT_PLAUSIBLE_API_KEY=

src/datasync/coat.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import dlt
2+
import typer
3+
from dlt.sources.helpers.rest_client import RESTClient
4+
from dlt.sources.helpers.rest_client.auth import BearerTokenAuth
5+
from dlt.sources.helpers.rest_client.paginators import OffsetPaginator
6+
7+
from .settings import log
8+
9+
app = typer.Typer(help="Export COAT data to parquet")
10+
11+
PLAUSIBLE_BASE_URL = "https://plausible.io/api/v2/query"
12+
PLAUSIBLE_METRICS = ["visitors", "visits", "pageviews", "bounce_rate", "visit_duration"]
13+
PLAUSIBLE_SITE_ID = "data.coat.no"
14+
15+
_BREAKDOWN_DIMENSIONS = [
16+
("page", "event:page"),
17+
("source", "visit:source"),
18+
("country", "visit:country_name"),
19+
("device", "visit:device"),
20+
("browser", "visit:browser"),
21+
("os", "visit:os"),
22+
]
23+
24+
25+
def plausible_paginate(api_key: str, base_payload: dict, page_size: int = 10_000):
26+
"""Yield all result rows from a Plausible v2 query using dlt pagination."""
27+
client = RESTClient(
28+
base_url=PLAUSIBLE_BASE_URL,
29+
headers={"Content-Type": "application/json"},
30+
auth=BearerTokenAuth(token=api_key),
31+
paginator=OffsetPaginator(
32+
limit=page_size,
33+
offset_body_path="pagination.offset",
34+
limit_body_path="pagination.limit",
35+
total_path="meta.total_rows",
36+
),
37+
data_selector="results",
38+
)
39+
40+
payload = {
41+
**base_payload,
42+
"include": {"total_rows": True},
43+
}
44+
for page in client.paginate(method="POST", json=payload, timeout=60):
45+
yield from page
46+
47+
48+
@dlt.resource(
49+
name="plausible_timeseries",
50+
primary_key="date",
51+
write_disposition="replace",
52+
)
53+
def plausible_timeseries(api_key: str, site_id: str = PLAUSIBLE_SITE_ID):
54+
"""Fetch daily timeseries stats from Plausible Analytics."""
55+
log.info(f"Fetching Plausible timeseries for {site_id}")
56+
for row in plausible_paginate(
57+
api_key,
58+
{
59+
"site_id": site_id,
60+
"metrics": PLAUSIBLE_METRICS,
61+
"date_range": "24h",
62+
"dimensions": ["time:day"],
63+
"order_by": [["time:day", "asc"]],
64+
},
65+
):
66+
keys = ["date", *PLAUSIBLE_METRICS]
67+
vals = [*row["dimensions"], *row["metrics"]]
68+
yield dict(zip(keys, vals, strict=True))
69+
70+
71+
@dlt.resource(
72+
name="plausible_breakdown",
73+
primary_key=["dimension", "value"],
74+
write_disposition="replace",
75+
)
76+
def plausible_breakdown(api_key: str, site_id: str = PLAUSIBLE_SITE_ID):
77+
"""Fetch breakdowns by page, source, country, device, browser, and OS."""
78+
for dim_name, dim_key in _BREAKDOWN_DIMENSIONS:
79+
log.info(f"Fetching Plausible breakdown: {dim_name} ({dim_key})")
80+
for row in plausible_paginate(
81+
api_key,
82+
{
83+
"site_id": site_id,
84+
"metrics": PLAUSIBLE_METRICS,
85+
"date_range": "24h",
86+
"dimensions": [dim_key],
87+
},
88+
):
89+
yield {
90+
"dimension": dim_name,
91+
"value": row["dimensions"][0],
92+
**dict(zip(PLAUSIBLE_METRICS, row["metrics"], strict=True)),
93+
}
94+
95+
96+
@dlt.source(name="coat_plausible", max_table_nesting=0)
97+
def plausible_source(api_key: str, site_id: str = PLAUSIBLE_SITE_ID):
98+
"""Define the Plausible Analytics data source for a COAT site."""
99+
return (
100+
plausible_timeseries(api_key=api_key, site_id=site_id),
101+
plausible_breakdown(api_key=api_key, site_id=site_id),
102+
)
103+
104+
105+
@app.command()
106+
def get_plausible_analytics(
107+
bucket_url: str = typer.Option(
108+
default="data",
109+
envvar="COAT_BUCKET_URL",
110+
help="Destination bucket URL (local path or s3://...)",
111+
),
112+
api_key: str = typer.Option(
113+
...,
114+
envvar="COAT_PLAUSIBLE_API_KEY",
115+
help="Plausible Stats API key",
116+
),
117+
site_id: str = typer.Option(
118+
default=PLAUSIBLE_SITE_ID,
119+
envvar="COAT_PLAUSIBLE_SITE_ID",
120+
help="Plausible site ID (e.g. data.coat.no)",
121+
),
122+
):
123+
"""Run the Plausible analytics pipeline for data.coat.no."""
124+
pipeline = dlt.pipeline(
125+
pipeline_name="coat_plausible",
126+
destination=dlt.destinations.filesystem(
127+
bucket_url=bucket_url, layout="{table_name}.{ext}"
128+
),
129+
dataset_name="coat_plausible",
130+
)
131+
pipeline.run(
132+
plausible_source(api_key=api_key, site_id=site_id),
133+
loader_file_format="parquet",
134+
)
135+
log.info(f"Plausible pipeline complete. Data at: {bucket_url}/coat_plausible")
136+
137+
138+
if __name__ == "__main__":
139+
app()

src/datasync/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import typer
66

77
from . import (
8+
coat,
89
dms,
910
gbif_backbone,
1011
grass,
@@ -30,6 +31,7 @@
3031
app.add_typer(gbif_backbone.app, name="gbif-backbone")
3132
app.add_typer(ipt_app, name="ipt")
3233
app.add_typer(mediebank.app, name="mediebank")
34+
app.add_typer(coat.app, name="coat")
3335

3436
if __name__ == "__main__":
3537
app()

0 commit comments

Comments
 (0)