Merge branch 'dev' into update-es-translations-and-fix-typo

This commit is contained in:
Israel Ortiz de Zárate 2024-07-23 17:46:46 +02:00 committed by GitHub
commit e5d176547a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 1819 additions and 1259 deletions

View File

@ -67,3 +67,202 @@ CREATE TABLE umami.session_data
engine = MergeTree engine = MergeTree
ORDER BY (website_id, session_id, data_key, created_at) ORDER BY (website_id, session_id, data_key, created_at)
SETTINGS index_granularity = 8192; SETTINGS index_granularity = 8192;
-- stats hourly
CREATE TABLE umami.website_event_stats_hourly
(
website_id UUID,
session_id UUID,
visit_id UUID,
hostname LowCardinality(String),
browser LowCardinality(String),
os LowCardinality(String),
device LowCardinality(String),
country LowCardinality(String),
subdivision1 LowCardinality(String),
city String,
entry_url AggregateFunction(argMin, String, DateTime('UTC')),
exit_url AggregateFunction(argMax, String, DateTime('UTC')),
url_path SimpleAggregateFunction(groupArrayArray, Array(String)),
url_query SimpleAggregateFunction(groupArrayArray, Array(String)),
referrer_domain SimpleAggregateFunction(groupArrayArray, Array(String)),
page_title SimpleAggregateFunction(groupArrayArray, Array(String)),
event_type UInt32,
event_name SimpleAggregateFunction(groupArrayArray, Array(String)),
views SimpleAggregateFunction(sum, UInt64),
min_time SimpleAggregateFunction(min, DateTime('UTC')),
max_time SimpleAggregateFunction(max, DateTime('UTC')),
created_at Datetime('UTC')
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (
website_id,
toStartOfHour(created_at),
cityHash64(visit_id),
visit_id
)
SAMPLE BY cityHash64(visit_id)
TTL created_at + INTERVAL 10 DAY;
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv
TO umami.website_event_stats_hourly
AS
SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
country,
subdivision1,
city,
entry_url,
exit_url,
url_paths as url_path,
url_query,
referrer_domain,
page_title,
event_type,
event_name,
views,
min_time,
max_time,
timestamp as created_at
FROM (SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
country,
subdivision1,
city,
argMinState(url_path, created_at) entry_url,
argMaxState(url_path, created_at) exit_url,
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
event_type,
if(event_type = 2, groupArray(event_name), []) event_name,
sumIf(1, event_type = 1) views,
min(created_at) min_time,
max(created_at) max_time,
toStartOfHour(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
country,
subdivision1,
city,
event_type,
timestamp);
-- stats daily
CREATE TABLE umami.website_event_stats_daily
(
website_id UUID,
session_id UUID,
visit_id UUID,
hostname LowCardinality(String),
browser LowCardinality(String),
os LowCardinality(String),
device LowCardinality(String),
country LowCardinality(String),
subdivision1 LowCardinality(String),
city String,
entry_url AggregateFunction(argMin, String, DateTime('UTC')),
exit_url AggregateFunction(argMax, String, DateTime('UTC')),
url_path SimpleAggregateFunction(groupArrayArray, Array(String)),
url_query SimpleAggregateFunction(groupArrayArray, Array(String)),
referrer_domain SimpleAggregateFunction(groupArrayArray, Array(String)),
page_title SimpleAggregateFunction(groupArrayArray, Array(String)),
event_type UInt32,
event_name SimpleAggregateFunction(groupArrayArray, Array(String)),
views SimpleAggregateFunction(sum, UInt64),
min_time SimpleAggregateFunction(min, DateTime('UTC')),
max_time SimpleAggregateFunction(max, DateTime('UTC')),
created_at Datetime('UTC')
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (
website_id,
toStartOfDay(created_at),
cityHash64(visit_id),
visit_id
)
SAMPLE BY cityHash64(visit_id);
CREATE MATERIALIZED VIEW umami.website_event_stats_daily_mv
TO umami.website_event_stats_daily
AS
SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
country,
subdivision1,
city,
entry_url,
exit_url,
url_paths as url_path,
url_query,
referrer_domain,
page_title,
event_type,
event_name,
views,
min_time,
max_time,
timestamp as created_at
FROM (SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
country,
subdivision1,
city,
argMinState(url_path, created_at) entry_url,
argMaxState(url_path, created_at) exit_url,
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
event_type,
if(event_type = 2, groupArray(event_name), []) event_name,
sumIf(1, event_type = 1) views,
min(created_at) min_time,
max(created_at) max_time,
toStartOfDay(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
country,
subdivision1,
city,
event_type,
timestamp);

View File

@ -64,9 +64,9 @@
".next/cache" ".next/cache"
], ],
"dependencies": { "dependencies": {
"@clickhouse/client": "^1.0.2", "@clickhouse/client": "^1.3.0",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
"@prisma/client": "5.14.0", "@prisma/client": "5.16.2",
"@prisma/extension-read-replicas": "^0.3.0", "@prisma/extension-read-replicas": "^0.3.0",
"@react-spring/web": "^9.7.3", "@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^5.28.6", "@tanstack/react-query": "^5.28.6",
@ -98,11 +98,11 @@
"maxmind": "^4.3.6", "maxmind": "^4.3.6",
"md5": "^2.3.0", "md5": "^2.3.0",
"moment-timezone": "^0.5.35", "moment-timezone": "^0.5.35",
"next": "14.2.3", "next": "14.2.4",
"next-basics": "^0.39.0", "next-basics": "^0.39.0",
"node-fetch": "^3.2.8", "node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prisma": "5.14.0", "prisma": "5.16.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-basics": "^0.123.0", "react-basics": "^0.123.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
@ -175,6 +175,6 @@
"tar": "^6.1.2", "tar": "^6.1.2",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.4.3" "typescript": "^5.5.3"
} }
} }

View File

@ -152,7 +152,7 @@
"label.compare": [ "label.compare": [
{ {
"type": 0, "type": 0,
"value": "Compare" "value": "Comparar"
} }
], ],
"label.confirm": [ "label.confirm": [
@ -182,7 +182,7 @@
"label.count": [ "label.count": [
{ {
"type": 0, "type": 0,
"value": "Count" "value": "Recompte"
} }
], ],
"label.countries": [ "label.countries": [
@ -236,7 +236,7 @@
"label.current": [ "label.current": [
{ {
"type": 0, "type": 0,
"value": "Current" "value": "Actual"
} }
], ],
"label.current-password": [ "label.current-password": [
@ -398,13 +398,13 @@
"label.end-step": [ "label.end-step": [
{ {
"type": 0, "type": 0,
"value": "End Step" "value": "Pas Final"
} }
], ],
"label.entry": [ "label.entry": [
{ {
"type": 0, "type": 0,
"value": "Entry URL" "value": "URL d'entrada"
} }
], ],
"label.event": [ "label.event": [
@ -428,7 +428,7 @@
"label.exit": [ "label.exit": [
{ {
"type": 0, "type": 0,
"value": "Exit URL" "value": "URL de sortida"
} }
], ],
"label.false": [ "label.false": [
@ -488,19 +488,19 @@
"label.goal": [ "label.goal": [
{ {
"type": 0, "type": 0,
"value": "Goal" "value": "Meta"
} }
], ],
"label.goals": [ "label.goals": [
{ {
"type": 0, "type": 0,
"value": "Goals" "value": "Metes"
} }
], ],
"label.goals-description": [ "label.goals-description": [
{ {
"type": 0, "type": 0,
"value": "Track your goals for pageviews and events." "value": "Feu un seguiment de les seves metes per a pàgines vistes i esdeveniments."
} }
], ],
"label.greater-than": [ "label.greater-than": [
@ -518,13 +518,13 @@
"label.host": [ "label.host": [
{ {
"type": 0, "type": 0,
"value": "Host" "value": "Amfitrió"
} }
], ],
"label.hosts": [ "label.hosts": [
{ {
"type": 0, "type": 0,
"value": "Hosts" "value": "Amfitrions"
} }
], ],
"label.insights": [ "label.insights": [
@ -578,13 +578,13 @@
"label.journey": [ "label.journey": [
{ {
"type": 0, "type": 0,
"value": "Journey" "value": "Trajecte"
} }
], ],
"label.journey-description": [ "label.journey-description": [
{ {
"type": 0, "type": 0,
"value": "Understand how users navigate through your website." "value": "Entengui com naveguen els usuaris pel seu lloc web."
} }
], ],
"label.language": [ "label.language": [
@ -777,7 +777,7 @@
"value": [ "value": [
{ {
"type": 0, "type": 0,
"value": "record" "value": "registre"
} }
] ]
}, },
@ -785,7 +785,7 @@
"value": [ "value": [
{ {
"type": 0, "type": 0,
"value": "records" "value": "registres"
} }
] ]
} }
@ -874,19 +874,19 @@
"label.previous": [ "label.previous": [
{ {
"type": 0, "type": 0,
"value": "Previous" "value": "Anterior"
} }
], ],
"label.previous-period": [ "label.previous-period": [
{ {
"type": 0, "type": 0,
"value": "Previous period" "value": "Període anterior"
} }
], ],
"label.previous-year": [ "label.previous-year": [
{ {
"type": 0, "type": 0,
"value": "Previous year" "value": "Any anterior"
} }
], ],
"label.profile": [ "label.profile": [
@ -898,7 +898,7 @@
"label.property": [ "label.property": [
{ {
"type": 0, "type": 0,
"value": "Property" "value": "Propietat"
} }
], ],
"label.queries": [ "label.queries": [
@ -1090,7 +1090,7 @@
"label.start-step": [ "label.start-step": [
{ {
"type": 0, "type": 0,
"value": "Start Step" "value": "Pas inicial"
} }
], ],
"label.steps": [ "label.steps": [
@ -1360,7 +1360,7 @@
"label.views-per-visit": [ "label.views-per-visit": [
{ {
"type": 0, "type": 0,
"value": "Views per visit" "value": "Vistes per visita"
} }
], ],
"label.visit-duration": [ "label.visit-duration": [
@ -1462,7 +1462,7 @@
"message.collected-data": [ "message.collected-data": [
{ {
"type": 0, "type": 0,
"value": "Collected data" "value": "Dades recol·lectades"
} }
], ],
"message.confirm-delete": [ "message.confirm-delete": [
@ -1790,7 +1790,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Els visitants han sortit" "value": "Visitants han sortit"
} }
] ]
} }

View File

@ -32,13 +32,13 @@
"label.add-member": [ "label.add-member": [
{ {
"type": 0, "type": 0,
"value": "Add member" "value": "Mitglied hinzufügen"
} }
], ],
"label.add-step": [ "label.add-step": [
{ {
"type": 0, "type": 0,
"value": "Add step" "value": "Schritt hinzufügen"
} }
], ],
"label.add-website": [ "label.add-website": [
@ -104,7 +104,7 @@
"label.breakdown": [ "label.breakdown": [
{ {
"type": 0, "type": 0,
"value": "Breakdown" "value": "Aufschlüsselung"
} }
], ],
"label.browser": [ "label.browser": [
@ -152,7 +152,7 @@
"label.compare": [ "label.compare": [
{ {
"type": 0, "type": 0,
"value": "Compare" "value": "Vergleich"
} }
], ],
"label.confirm": [ "label.confirm": [
@ -182,7 +182,7 @@
"label.count": [ "label.count": [
{ {
"type": 0, "type": 0,
"value": "Count" "value": "Anzahl"
} }
], ],
"label.countries": [ "label.countries": [
@ -200,7 +200,7 @@
"label.create": [ "label.create": [
{ {
"type": 0, "type": 0,
"value": "Create" "value": "Erstellen"
} }
], ],
"label.create-report": [ "label.create-report": [
@ -230,13 +230,13 @@
"label.created-by": [ "label.created-by": [
{ {
"type": 0, "type": 0,
"value": "Created By" "value": "Erstellt von"
} }
], ],
"label.current": [ "label.current": [
{ {
"type": 0, "type": 0,
"value": "Current" "value": "Aktuell"
} }
], ],
"label.current-password": [ "label.current-password": [
@ -296,7 +296,7 @@
"label.delete-report": [ "label.delete-report": [
{ {
"type": 0, "type": 0,
"value": "Delete report" "value": "Bericht löschen"
} }
], ],
"label.delete-team": [ "label.delete-team": [
@ -386,7 +386,7 @@
"label.edit-member": [ "label.edit-member": [
{ {
"type": 0, "type": 0,
"value": "Edit member" "value": "Mitglied bearbeiten"
} }
], ],
"label.enable-share-url": [ "label.enable-share-url": [
@ -398,7 +398,7 @@
"label.end-step": [ "label.end-step": [
{ {
"type": 0, "type": 0,
"value": "End Step" "value": "Schritt beenden"
} }
], ],
"label.entry": [ "label.entry": [
@ -482,25 +482,25 @@
"label.funnel-description": [ "label.funnel-description": [
{ {
"type": 0, "type": 0,
"value": "Understand the conversion and drop-off rate of users." "value": "Verstehe die Konversions- und Dropoffrate von Nutzern."
} }
], ],
"label.goal": [ "label.goal": [
{ {
"type": 0, "type": 0,
"value": "Goal" "value": "Ziel"
} }
], ],
"label.goals": [ "label.goals": [
{ {
"type": 0, "type": 0,
"value": "Goals" "value": "Ziele"
} }
], ],
"label.goals-description": [ "label.goals-description": [
{ {
"type": 0, "type": 0,
"value": "Track your goals for pageviews and events." "value": "Verfolgen Sie Ihre Ziele für Aufrufe und Events."
} }
], ],
"label.greater-than": [ "label.greater-than": [
@ -536,7 +536,7 @@
"label.insights-description": [ "label.insights-description": [
{ {
"type": 0, "type": 0,
"value": "Dive deeper into your data by using segments and filters." "value": "Tauchen Sie tiefer in Ihre Daten mit Filtern und Segmenten ein."
} }
], ],
"label.is": [ "label.is": [
@ -584,7 +584,7 @@
"label.journey-description": [ "label.journey-description": [
{ {
"type": 0, "type": 0,
"value": "Understand how users navigate through your website." "value": "Verstehen Sie, wie Nutzer Ihre Website navigieren."
} }
], ],
"label.language": [ "label.language": [
@ -686,7 +686,7 @@
"label.manage": [ "label.manage": [
{ {
"type": 0, "type": 0,
"value": "Manage" "value": "Verwalten"
} }
], ],
"label.manager": [ "label.manager": [
@ -704,7 +704,7 @@
"label.member": [ "label.member": [
{ {
"type": 0, "type": 0,
"value": "Member" "value": "Mitglied"
} }
], ],
"label.members": [ "label.members": [
@ -734,7 +734,7 @@
"label.my-account": [ "label.my-account": [
{ {
"type": 0, "type": 0,
"value": "My account" "value": "Mein Konto"
} }
], ],
"label.my-websites": [ "label.my-websites": [
@ -874,19 +874,19 @@
"label.previous": [ "label.previous": [
{ {
"type": 0, "type": 0,
"value": "Previous" "value": "Vorherige"
} }
], ],
"label.previous-period": [ "label.previous-period": [
{ {
"type": 0, "type": 0,
"value": "Previous period" "value": "Vorheriger Zeitraum"
} }
], ],
"label.previous-year": [ "label.previous-year": [
{ {
"type": 0, "type": 0,
"value": "Previous year" "value": "Vorheriges Jahr"
} }
], ],
"label.profile": [ "label.profile": [
@ -898,7 +898,7 @@
"label.property": [ "label.property": [
{ {
"type": 0, "type": 0,
"value": "Property" "value": "Besitz"
} }
], ],
"label.queries": [ "label.queries": [
@ -970,7 +970,7 @@
"label.remove-member": [ "label.remove-member": [
{ {
"type": 0, "type": 0,
"value": "Remove member" "value": "Mitglied entfernen"
} }
], ],
"label.reports": [ "label.reports": [
@ -1006,7 +1006,7 @@
"label.retention-description": [ "label.retention-description": [
{ {
"type": 0, "type": 0,
"value": "Measure your website stickiness by tracking how often users return." "value": "Messen Sie die Presenz Ihrer Website, indem Sie tracken wie oft Nutzer zurückkehren."
} }
], ],
"label.role": [ "label.role": [
@ -1036,13 +1036,13 @@
"label.search": [ "label.search": [
{ {
"type": 0, "type": 0,
"value": "Search" "value": "Suche"
} }
], ],
"label.select": [ "label.select": [
{ {
"type": 0, "type": 0,
"value": "Select" "value": "Auswählen"
} }
], ],
"label.select-date": [ "label.select-date": [
@ -1054,7 +1054,7 @@
"label.select-role": [ "label.select-role": [
{ {
"type": 0, "type": 0,
"value": "Select role" "value": "Rolle auswählen"
} }
], ],
"label.select-website": [ "label.select-website": [
@ -1090,13 +1090,13 @@
"label.start-step": [ "label.start-step": [
{ {
"type": 0, "type": 0,
"value": "Start Step" "value": "Schritt starten"
} }
], ],
"label.steps": [ "label.steps": [
{ {
"type": 0, "type": 0,
"value": "Steps" "value": "Schritte"
} }
], ],
"label.sum": [ "label.sum": [
@ -1126,7 +1126,7 @@
"label.team-manager": [ "label.team-manager": [
{ {
"type": 0, "type": 0,
"value": "Team manager" "value": "Team-Manager"
} }
], ],
"label.team-member": [ "label.team-member": [
@ -1234,13 +1234,13 @@
"label.transfer": [ "label.transfer": [
{ {
"type": 0, "type": 0,
"value": "Transfer" "value": "Übertragung"
} }
], ],
"label.transfer-website": [ "label.transfer-website": [
{ {
"type": 0, "type": 0,
"value": "Transfer website" "value": "Website übertragen"
} }
], ],
"label.true": [ "label.true": [
@ -1324,7 +1324,7 @@
"label.utm-description": [ "label.utm-description": [
{ {
"type": 0, "type": 0,
"value": "Track your campaigns through UTM parameters." "value": "Tracken Sie Ihre Kampagnen mit Hilfe von UTM Parametern."
} }
], ],
"label.value": [ "label.value": [
@ -1360,7 +1360,7 @@
"label.views-per-visit": [ "label.views-per-visit": [
{ {
"type": 0, "type": 0,
"value": "Views per visit" "value": "Aufrufe pro Besuch"
} }
], ],
"label.visit-duration": [ "label.visit-duration": [
@ -1378,7 +1378,7 @@
"label.visits": [ "label.visits": [
{ {
"type": 0, "type": 0,
"value": "Visits" "value": "Besuche"
} }
], ],
"label.website": [ "label.website": [
@ -1414,7 +1414,7 @@
"message.action-confirmation": [ "message.action-confirmation": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "Tippen Sie "
}, },
{ {
"type": 1, "type": 1,
@ -1422,7 +1422,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " in das untenliegende Feld, um zu bestätigen."
} }
], ],
"message.active-users": [ "message.active-users": [
@ -1462,7 +1462,7 @@
"message.collected-data": [ "message.collected-data": [
{ {
"type": 0, "type": 0,
"value": "Collected data" "value": "Gesammelte Daten"
} }
], ],
"message.confirm-delete": [ "message.confirm-delete": [
@ -1496,7 +1496,7 @@
"message.confirm-remove": [ "message.confirm-remove": [
{ {
"type": 0, "type": 0,
"value": "Are you sure you want to remove " "value": "Sind Sie sicher, dass Sie "
}, },
{ {
"type": 1, "type": 1,
@ -1504,7 +1504,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": "?" "value": " entfernen möchten?"
} }
], ],
"message.confirm-reset": [ "message.confirm-reset": [
@ -1524,7 +1524,7 @@
"message.delete-team-warning": [ "message.delete-team-warning": [
{ {
"type": 0, "type": 0,
"value": "Deleting a team will also delete all team websites." "value": "Alle zugehörigen Websiten werden ebenfalls gelöscht."
} }
], ],
"message.delete-website-warning": [ "message.delete-website-warning": [
@ -1708,25 +1708,25 @@
"message.transfer-team-website-to-user": [ "message.transfer-team-website-to-user": [
{ {
"type": 0, "type": 0,
"value": "Transfer this website to your account?" "value": "Möchten Sie diese Website auf Ihr Konto übertragen?"
} }
], ],
"message.transfer-user-website-to-team": [ "message.transfer-user-website-to-team": [
{ {
"type": 0, "type": 0,
"value": "Select the team to transfer this website to." "value": "Wählen Sie das Team, auf das die Website übertragen wird."
} }
], ],
"message.transfer-website": [ "message.transfer-website": [
{ {
"type": 0, "type": 0,
"value": "Transfer website ownership to your account or another team." "value": "Übertragen Sie den Besitz der Website auf Ihren Account oder ein anderes Team."
} }
], ],
"message.triggered-event": [ "message.triggered-event": [
{ {
"type": 0, "type": 0,
"value": "Triggered event" "value": "Event ausgelöst"
} }
], ],
"message.user-deleted": [ "message.user-deleted": [
@ -1738,7 +1738,7 @@
"message.viewed-page": [ "message.viewed-page": [
{ {
"type": 0, "type": 0,
"value": "Viewed page" "value": "Seite besucht"
} }
], ],
"message.visitor-log": [ "message.visitor-log": [
@ -1778,7 +1778,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors dropped off" "value": "Besucher haben die Seite verlassen"
} }
] ]
} }

File diff suppressed because it is too large Load Diff

View File

@ -152,7 +152,7 @@
"label.compare": [ "label.compare": [
{ {
"type": 0, "type": 0,
"value": "Compare" "value": "Porównaj"
} }
], ],
"label.confirm": [ "label.confirm": [
@ -182,7 +182,7 @@
"label.count": [ "label.count": [
{ {
"type": 0, "type": 0,
"value": "Count" "value": "Liczba"
} }
], ],
"label.countries": [ "label.countries": [
@ -236,7 +236,7 @@
"label.current": [ "label.current": [
{ {
"type": 0, "type": 0,
"value": "Current" "value": "Aktualny"
} }
], ],
"label.current-password": [ "label.current-password": [
@ -398,7 +398,7 @@
"label.end-step": [ "label.end-step": [
{ {
"type": 0, "type": 0,
"value": "End Step" "value": "Krok końcowy"
} }
], ],
"label.entry": [ "label.entry": [
@ -428,7 +428,7 @@
"label.exit": [ "label.exit": [
{ {
"type": 0, "type": 0,
"value": "Exit URL" "value": "URL wyjściowy"
} }
], ],
"label.false": [ "label.false": [
@ -488,13 +488,13 @@
"label.goal": [ "label.goal": [
{ {
"type": 0, "type": 0,
"value": "Goal" "value": "Cel"
} }
], ],
"label.goals": [ "label.goals": [
{ {
"type": 0, "type": 0,
"value": "Goals" "value": "Cele"
} }
], ],
"label.goals-description": [ "label.goals-description": [
@ -524,7 +524,7 @@
"label.hosts": [ "label.hosts": [
{ {
"type": 0, "type": 0,
"value": "Hosts" "value": "Hosty"
} }
], ],
"label.insights": [ "label.insights": [
@ -578,13 +578,13 @@
"label.journey": [ "label.journey": [
{ {
"type": 0, "type": 0,
"value": "Journey" "value": "Droga"
} }
], ],
"label.journey-description": [ "label.journey-description": [
{ {
"type": 0, "type": 0,
"value": "Understand how users navigate through your website." "value": "Zrozum, w jaki sposób użytkownicy poruszają się po Twojej witrynie."
} }
], ],
"label.language": [ "label.language": [
@ -874,19 +874,19 @@
"label.previous": [ "label.previous": [
{ {
"type": 0, "type": 0,
"value": "Previous" "value": "Poprzedni"
} }
], ],
"label.previous-period": [ "label.previous-period": [
{ {
"type": 0, "type": 0,
"value": "Previous period" "value": "Poprzedni okres"
} }
], ],
"label.previous-year": [ "label.previous-year": [
{ {
"type": 0, "type": 0,
"value": "Previous year" "value": "Poprzedni rok"
} }
], ],
"label.profile": [ "label.profile": [
@ -928,7 +928,7 @@
"label.referrer": [ "label.referrer": [
{ {
"type": 0, "type": 0,
"value": "Referrer" "value": "Źródło odsyłające"
} }
], ],
"label.referrers": [ "label.referrers": [
@ -970,7 +970,7 @@
"label.remove-member": [ "label.remove-member": [
{ {
"type": 0, "type": 0,
"value": "Remove member" "value": "Usuń członka"
} }
], ],
"label.reports": [ "label.reports": [
@ -1090,7 +1090,7 @@
"label.start-step": [ "label.start-step": [
{ {
"type": 0, "type": 0,
"value": "Start Step" "value": "Krok startowy"
} }
], ],
"label.steps": [ "label.steps": [
@ -1378,7 +1378,7 @@
"label.visits": [ "label.visits": [
{ {
"type": 0, "type": 0,
"value": "Odwiedząjący" "value": "Wizyty"
} }
], ],
"label.website": [ "label.website": [
@ -1462,7 +1462,7 @@
"message.collected-data": [ "message.collected-data": [
{ {
"type": 0, "type": 0,
"value": "Collected data" "value": "Zebrane dane"
} }
], ],
"message.confirm-delete": [ "message.confirm-delete": [

View File

@ -152,7 +152,7 @@
"label.compare": [ "label.compare": [
{ {
"type": 0, "type": 0,
"value": "Compare" "value": "比较"
} }
], ],
"label.confirm": [ "label.confirm": [
@ -182,7 +182,7 @@
"label.count": [ "label.count": [
{ {
"type": 0, "type": 0,
"value": "Count" "value": "统计"
} }
], ],
"label.countries": [ "label.countries": [
@ -236,7 +236,7 @@
"label.current": [ "label.current": [
{ {
"type": 0, "type": 0,
"value": "Current" "value": "目前"
} }
], ],
"label.current-password": [ "label.current-password": [
@ -398,13 +398,13 @@
"label.end-step": [ "label.end-step": [
{ {
"type": 0, "type": 0,
"value": "End Step" "value": "结束步骤"
} }
], ],
"label.entry": [ "label.entry": [
{ {
"type": 0, "type": 0,
"value": "Entry URL" "value": "入口 URL"
} }
], ],
"label.event": [ "label.event": [
@ -428,7 +428,7 @@
"label.exit": [ "label.exit": [
{ {
"type": 0, "type": 0,
"value": "Exit URL" "value": "退出 URL"
} }
], ],
"label.false": [ "label.false": [
@ -488,19 +488,19 @@
"label.goal": [ "label.goal": [
{ {
"type": 0, "type": 0,
"value": "Goal" "value": "目标"
} }
], ],
"label.goals": [ "label.goals": [
{ {
"type": 0, "type": 0,
"value": "Goals" "value": "目标"
} }
], ],
"label.goals-description": [ "label.goals-description": [
{ {
"type": 0, "type": 0,
"value": "Track your goals for pageviews and events." "value": "跟踪页面浏览量和事件的目标。"
} }
], ],
"label.greater-than": [ "label.greater-than": [
@ -518,13 +518,13 @@
"label.host": [ "label.host": [
{ {
"type": 0, "type": 0,
"value": "Host" "value": "主机"
} }
], ],
"label.hosts": [ "label.hosts": [
{ {
"type": 0, "type": 0,
"value": "Hosts" "value": "主机"
} }
], ],
"label.insights": [ "label.insights": [
@ -578,13 +578,13 @@
"label.journey": [ "label.journey": [
{ {
"type": 0, "type": 0,
"value": "Journey" "value": "用户浏览轨迹"
} }
], ],
"label.journey-description": [ "label.journey-description": [
{ {
"type": 0, "type": 0,
"value": "Understand how users navigate through your website." "value": "了解用户如何浏览网站。"
} }
], ],
"label.language": [ "label.language": [
@ -692,7 +692,7 @@
"label.manager": [ "label.manager": [
{ {
"type": 0, "type": 0,
"value": "Manager" "value": "管理者"
} }
], ],
"label.max": [ "label.max": [
@ -798,7 +798,7 @@
"label.ok": [ "label.ok": [
{ {
"type": 0, "type": 0,
"value": "OK" "value": "好的"
} }
], ],
"label.os": [ "label.os": [
@ -882,19 +882,19 @@
"label.previous": [ "label.previous": [
{ {
"type": 0, "type": 0,
"value": "Previous" "value": "先前"
} }
], ],
"label.previous-period": [ "label.previous-period": [
{ {
"type": 0, "type": 0,
"value": "Previous period" "value": "上一时期"
} }
], ],
"label.previous-year": [ "label.previous-year": [
{ {
"type": 0, "type": 0,
"value": "Previous year" "value": "上一年"
} }
], ],
"label.profile": [ "label.profile": [
@ -906,7 +906,7 @@
"label.property": [ "label.property": [
{ {
"type": 0, "type": 0,
"value": "Property" "value": "属性"
} }
], ],
"label.queries": [ "label.queries": [
@ -1098,7 +1098,7 @@
"label.start-step": [ "label.start-step": [
{ {
"type": 0, "type": 0,
"value": "Start Step" "value": "开始步骤"
} }
], ],
"label.steps": [ "label.steps": [
@ -1134,7 +1134,7 @@
"label.team-manager": [ "label.team-manager": [
{ {
"type": 0, "type": 0,
"value": "Team manager" "value": "团队管理者"
} }
], ],
"label.team-member": [ "label.team-member": [
@ -1450,7 +1450,7 @@
"message.collected-data": [ "message.collected-data": [
{ {
"type": 0, "type": 0,
"value": "Collected data" "value": "已收集的数据"
} }
], ],
"message.confirm-delete": [ "message.confirm-delete": [

View File

@ -44,7 +44,7 @@ async function checkConnection() {
success('Database connection successful.'); success('Database connection successful.');
} catch (e) { } catch (e) {
throw new Error('Unable to connect to the database.'); throw new Error('Unable to connect to the database: ' + e.message);
} }
} }

View File

@ -2,7 +2,6 @@ require('dotenv').config();
const cli = require('next/dist/cli/next-start'); const cli = require('next/dist/cli/next-start');
cli.nextStart({ cli.nextStart({
'--port': process.env.PORT || 3000, port: process.env.PORT || 3000,
'--hostname': process.env.HOSTNAME || '0.0.0.0', hostname: process.env.HOSTNAME || '0.0.0.0',
_: [],
}); });

View File

@ -21,8 +21,12 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
router.push(`/console/${value}`); router.push(`/console/${value}`);
} }
function handleClick() { function handleRunScript() {
window['umami'].track({ url: '/page-view', referrer: 'https://www.google.com' }); window['umami'].track(props => ({
...props,
url: '/page-view',
referrer: 'https://www.google.com',
}));
window['umami'].track('track-event-no-data'); window['umami'].track('track-event-no-data');
window['umami'].track('track-event-with-data', { window['umami'].track('track-event-with-data', {
test: 'test-data', test: 'test-data',
@ -44,7 +48,7 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
}); });
} }
function handleIdentifyClick() { function handleRunIdentify() {
window['umami'].identify({ window['umami'].identify({
userId: 123, userId: 123,
name: 'brian', name: 'brian',
@ -145,10 +149,10 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
</div> </div>
<div className={styles.group}> <div className={styles.group}>
<div className={styles.header}>Javascript events</div> <div className={styles.header}>Javascript events</div>
<Button id="manual-button" variant="primary" onClick={handleClick}> <Button id="manual-button" variant="primary" onClick={handleRunScript}>
Run script Run script
</Button> </Button>
<Button id="manual-button" variant="primary" onClick={handleIdentifyClick}> <Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
Run identify Run identify
</Button> </Button>
</div> </div>

View File

@ -51,6 +51,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
url: renderTeamUrl('/reports/journey'), url: renderTeamUrl('/reports/journey'),
icon: <Path />, icon: <Path />,
}, },
{
title: formatMessage(labels.revenue),
description: formatMessage(labels.revenueDescription),
url: renderTeamUrl('/reports/revenue'),
icon: <Path />,
},
]; ];
return ( return (

View File

@ -34,6 +34,10 @@
background-color: var(--base100); background-color: var(--base100);
} }
.step:last-child::before {
display: none;
}
.card { .card {
display: grid; display: grid;
gap: 20px; gap: 20px;

View File

@ -46,9 +46,14 @@ export function WebsiteHeader({
path: '/reports', path: '/reports',
}, },
{ {
label: formatMessage(labels.eventData), label: formatMessage(labels.sessions),
icon: <Icons.User />,
path: '/sessions',
},
{
label: formatMessage(labels.events),
icon: <Icons.Nodes />, icon: <Icons.Nodes />,
path: '/event-data', path: '/events',
}, },
]; ];

View File

@ -54,7 +54,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
}, },
]; ];
const getTime = ({ timestamp }) => format(timestamp * 1000, 'h:mm:ss'); const getTime = ({ createdAt }) => format(new Date(createdAt), 'h:mm:ss');
const getColor = ({ id, sessionId }) => stringToColor(sessionId || id); const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
@ -144,9 +144,12 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
const { events, visitors } = data; const { events, visitors } = data;
let logs = [ let logs = [
...events.map(e => ({ __type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW, ...e })), ...events.map(e => ({
__type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW,
...e,
})),
...visitors.map(v => ({ __type: TYPE_SESSION, ...v })), ...visitors.map(v => ({ __type: TYPE_SESSION, ...v })),
].sort(thenby.firstBy('timestamp', -1)); ].sort(thenby.firstBy('createdAt', -1));
if (search) { if (search) {
logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => { logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => {

View File

@ -0,0 +1,5 @@
export function SessionMetricsBar({ websiteId }: { websiteId: string }) {
return <h1>{websiteId}</h1>;
}
export default SessionMetricsBar;

View File

@ -0,0 +1,25 @@
import { useSessions } from 'components/hooks';
import SessionsTable from './SessionsTable';
import DataTable from 'components/common/DataTable';
import { ReactNode } from 'react';
export default function SessionsDataTable({
websiteId,
children,
}: {
websiteId?: string;
teamId?: string;
children?: ReactNode;
}) {
const queryResult = useSessions(websiteId);
if (queryResult?.result?.data?.length === 0) {
return children;
}
return (
<DataTable queryResult={queryResult} allowSearch={false}>
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />}
</DataTable>
);
}

View File

@ -0,0 +1,14 @@
'use client';
import WebsiteHeader from '../WebsiteHeader';
import SessionsDataTable from './SessionsDataTable';
export function SessionsPage({ websiteId }) {
return (
<>
<WebsiteHeader websiteId={websiteId} />
<SessionsDataTable websiteId={websiteId} />
</>
);
}
export default SessionsPage;

View File

@ -0,0 +1,31 @@
import { GridColumn, GridTable, useBreakpoint } from 'react-basics';
import { useFormat, useMessages } from 'components/hooks';
import { formatDistanceToNow } from 'date-fns';
export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) {
const { formatMessage, labels } = useMessages();
const breakpoint = useBreakpoint();
const { formatValue } = useFormat();
return (
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
<GridColumn name="id" label="ID" />
<GridColumn name="country" label={formatMessage(labels.country)}>
{row => formatValue(row.country, 'country')}
</GridColumn>
<GridColumn name="city" label={formatMessage(labels.city)} />
<GridColumn name="browser" label={formatMessage(labels.browser)}>
{row => formatValue(row.browser, 'browser')}
</GridColumn>
<GridColumn name="os" label={formatMessage(labels.os)} />
<GridColumn name="device" label={formatMessage(labels.device)}>
{row => formatValue(row.device, 'device')}
</GridColumn>
<GridColumn name="createdAt" label={formatMessage(labels.created)}>
{row => formatDistanceToNow(new Date(row.createdAt))}
</GridColumn>
</GridTable>
);
}
export default SessionsTable;

View File

@ -0,0 +1,10 @@
import SessionsPage from './SessionsPage';
import { Metadata } from 'next';
export default function ({ params: { websiteId } }) {
return <SessionsPage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'Sessions',
};

View File

@ -0,0 +1,28 @@
import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants';
export async function GET() {
if (
process.env.NODE_ENV !== 'production' &&
process.env.DISABLE_TELEMETRY &&
process.env.PRIVATE_MODE
) {
const script = `
(()=>{const i=document.createElement('img');
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
document.body.appendChild(i);})();
`;
return new Response(script.replace(/\s\s+/g, ''), {
headers: {
'content-type': 'text/javascript',
},
});
}
return new Response('/* telemetry disabled */', {
headers: {
'content-type': 'text/javascript',
},
});
}

View File

@ -79,7 +79,7 @@ export function Chart({
}; };
const updateChart = (data: any) => { const updateChart = (data: any) => {
if (data.datasets.length === chart.current.data.datasets.length) { if ((data?.datasets?.length || 0) === chart.current.data.datasets.length) {
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => { chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
if (data?.datasets[index]) { if (data?.datasets[index]) {
dataset.data = data?.datasets[index]?.data; dataset.data = data?.datasets[index]?.data;

View File

@ -5,6 +5,7 @@ export * from './queries/useLogin';
export * from './queries/useRealtime'; export * from './queries/useRealtime';
export * from './queries/useReport'; export * from './queries/useReport';
export * from './queries/useReports'; export * from './queries/useReports';
export * from './queries/useSessions';
export * from './queries/useShareToken'; export * from './queries/useShareToken';
export * from './queries/useTeam'; export * from './queries/useTeam';
export * from './queries/useTeams'; export * from './queries/useTeams';

View File

@ -0,0 +1,20 @@
import { useApi } from './useApi';
import { useFilterQuery } from './useFilterQuery';
import useModified from '../useModified';
export function useSessions(websiteId: string, params?: { [key: string]: string | number }) {
const { get } = useApi();
const { modified } = useModified(`websites`);
return useFilterQuery({
queryKey: ['sessions', { websiteId, modified, ...params }],
queryFn: (data: any) => {
return get(`/websites/${websiteId}/sessions`, {
...data,
...params,
});
},
});
}
export default useSessions;

View File

@ -1,5 +1,4 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Loading } from 'react-basics';
import { colord } from 'colord'; import { colord } from 'colord';
import BarChart from 'components/charts/BarChart'; import BarChart from 'components/charts/BarChart';
import { getDateArray } from 'lib/date'; import { getDateArray } from 'lib/date';
@ -51,10 +50,6 @@ export function EventsChart({ websiteId, className }: EventsChartProps) {
}; };
}, [data, startDate, endDate, unit]); }, [data, startDate, endDate, unit]);
if (isLoading) {
return <Loading icon="dots" />;
}
return ( return (
<BarChart <BarChart
className={className} className={className}

View File

@ -24,12 +24,12 @@
"label.cities": "Ciutats", "label.cities": "Ciutats",
"label.city": "Ciutat", "label.city": "Ciutat",
"label.clear-all": "Netejar tot", "label.clear-all": "Netejar tot",
"label.compare": "Compare", "label.compare": "Comparar",
"label.confirm": "Confirmar", "label.confirm": "Confirmar",
"label.confirm-password": "Confirma la contrasenya", "label.confirm-password": "Confirma la contrasenya",
"label.contains": "Conté", "label.contains": "Conté",
"label.continue": "Continuar", "label.continue": "Continuar",
"label.count": "Count", "label.count": "Recompte",
"label.countries": "Països", "label.countries": "Països",
"label.country": "País", "label.country": "País",
"label.create": "Crear", "label.create": "Crear",
@ -38,7 +38,7 @@
"label.create-user": "Crear usuari", "label.create-user": "Crear usuari",
"label.created": "Creat", "label.created": "Creat",
"label.created-by": "Creat Per", "label.created-by": "Creat Per",
"label.current": "Current", "label.current": "Actual",
"label.current-password": "Contrasenya actual", "label.current-password": "Contrasenya actual",
"label.custom-range": "Rang personalitzat", "label.custom-range": "Rang personalitzat",
"label.dashboard": "Panell", "label.dashboard": "Panell",
@ -65,12 +65,12 @@
"label.edit-dashboard": "Edita panell", "label.edit-dashboard": "Edita panell",
"label.edit-member": "Edita membre", "label.edit-member": "Edita membre",
"label.enable-share-url": "Activa l'enllaç per compartir", "label.enable-share-url": "Activa l'enllaç per compartir",
"label.end-step": "End Step", "label.end-step": "Pas Final",
"label.entry": "Entry URL", "label.entry": "URL d'entrada",
"label.event": "Esdeveniment", "label.event": "Esdeveniment",
"label.event-data": "Dades de l'esdeveniment", "label.event-data": "Dades de l'esdeveniment",
"label.events": "Esdeveniments", "label.events": "Esdeveniments",
"label.exit": "Exit URL", "label.exit": "URL de sortida",
"label.false": "Fals", "label.false": "Fals",
"label.field": "Camp", "label.field": "Camp",
"label.fields": "Camps", "label.fields": "Camps",
@ -80,13 +80,13 @@
"label.filters": "Filtres", "label.filters": "Filtres",
"label.funnel": "Embut", "label.funnel": "Embut",
"label.funnel-description": "Entengui la taxa de conversió i abandonament dels usuaris.", "label.funnel-description": "Entengui la taxa de conversió i abandonament dels usuaris.",
"label.goal": "Goal", "label.goal": "Meta",
"label.goals": "Goals", "label.goals": "Metes",
"label.goals-description": "Track your goals for pageviews and events.", "label.goals-description": "Feu un seguiment de les seves metes per a pàgines vistes i esdeveniments.",
"label.greater-than": "Més gran que", "label.greater-than": "Més gran que",
"label.greater-than-equals": "Més gran que o igual a", "label.greater-than-equals": "Més gran que o igual a",
"label.host": "Host", "label.host": "Amfitrió",
"label.hosts": "Hosts", "label.hosts": "Amfitrions",
"label.insights": "Insights", "label.insights": "Insights",
"label.insights-description": "Aprofundeixi en les seves dades mitjançant l'ús de segments i filtres.", "label.insights-description": "Aprofundeixi en les seves dades mitjançant l'ús de segments i filtres.",
"label.is": "És igual a", "label.is": "És igual a",
@ -95,8 +95,8 @@
"label.is-set": "Està establert", "label.is-set": "Està establert",
"label.join": "Unir", "label.join": "Unir",
"label.join-team": "Unir-se al equip", "label.join-team": "Unir-se al equip",
"label.journey": "Journey", "label.journey": "Trajecte",
"label.journey-description": "Understand how users navigate through your website.", "label.journey-description": "Entengui com naveguen els usuaris pel seu lloc web.",
"label.language": "Idioma", "label.language": "Idioma",
"label.languages": "Idiomes", "label.languages": "Idiomes",
"label.laptop": "Portàtil", "label.laptop": "Portàtil",
@ -122,7 +122,7 @@
"label.name": "Nom", "label.name": "Nom",
"label.new-password": "Contrasenya nova", "label.new-password": "Contrasenya nova",
"label.none": "Cap", "label.none": "Cap",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}", "label.number-of-records": "{x} {x, plural, one {registre} other {registres}}",
"label.ok": "OK", "label.ok": "OK",
"label.os": "SO", "label.os": "SO",
"label.overview": "Resum", "label.overview": "Resum",
@ -133,11 +133,11 @@
"label.pages": "Pàgines", "label.pages": "Pàgines",
"label.password": "Contrasenya", "label.password": "Contrasenya",
"label.powered-by": "Funciona amb {name}", "label.powered-by": "Funciona amb {name}",
"label.previous": "Previous", "label.previous": "Anterior",
"label.previous-period": "Previous period", "label.previous-period": "Període anterior",
"label.previous-year": "Previous year", "label.previous-year": "Any anterior",
"label.profile": "Perfil", "label.profile": "Perfil",
"label.property": "Property", "label.property": "Propietat",
"label.queries": "Consultes", "label.queries": "Consultes",
"label.query": "Consulta", "label.query": "Consulta",
"label.query-parameters": "Paràmetres de consulta", "label.query-parameters": "Paràmetres de consulta",
@ -169,7 +169,7 @@
"label.settings": "Configuració", "label.settings": "Configuració",
"label.share-url": "Enllaç per compartir", "label.share-url": "Enllaç per compartir",
"label.single-day": "Un sol dia", "label.single-day": "Un sol dia",
"label.start-step": "Start Step", "label.start-step": "Pas inicial",
"label.steps": "Pasos", "label.steps": "Pasos",
"label.sum": "Suma", "label.sum": "Suma",
"label.tablet": "Tauleta", "label.tablet": "Tauleta",
@ -214,7 +214,7 @@
"label.view-details": "Veure els detalls", "label.view-details": "Veure els detalls",
"label.view-only": "Només veure", "label.view-only": "Només veure",
"label.views": "Vistes", "label.views": "Vistes",
"label.views-per-visit": "Views per visit", "label.views-per-visit": "Vistes per visita",
"label.visit-duration": "Temps mitjà de visita", "label.visit-duration": "Temps mitjà de visita",
"label.visitors": "Visitants", "label.visitors": "Visitants",
"label.visits": "Visites", "label.visits": "Visites",
@ -225,7 +225,7 @@
"label.yesterday": "Ahir", "label.yesterday": "Ahir",
"message.action-confirmation": "Escrigui {confirmation} al cuadre inferior per confirmar.", "message.action-confirmation": "Escrigui {confirmation} al cuadre inferior per confirmar.",
"message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}", "message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}",
"message.collected-data": "Collected data", "message.collected-data": "Dades recol·lectades",
"message.confirm-delete": "Segur que vol esborrar {target}?", "message.confirm-delete": "Segur que vol esborrar {target}?",
"message.confirm-leave": "Segur que vol abandonar {target}?", "message.confirm-leave": "Segur que vol abandonar {target}?",
"message.confirm-remove": "Segur que vol eliminar {target}?", "message.confirm-remove": "Segur que vol eliminar {target}?",
@ -263,5 +263,5 @@
"message.user-deleted": "Usuari eliminat.", "message.user-deleted": "Usuari eliminat.",
"message.viewed-page": "Pàgina vista", "message.viewed-page": "Pàgina vista",
"message.visitor-log": "Visitant de {country} usant {browser} a {os} {device}", "message.visitor-log": "Visitant de {country} usant {browser} a {os} {device}",
"message.visitors-dropped-off": "Els visitants han sortit" "message.visitors-dropped-off": "Visitants han sortit"
} }

View File

@ -4,8 +4,8 @@
"label.activity-log": "Aktivitätsverlauf", "label.activity-log": "Aktivitätsverlauf",
"label.add": "Hinzufügen", "label.add": "Hinzufügen",
"label.add-description": "Beschreibung hinzufügen", "label.add-description": "Beschreibung hinzufügen",
"label.add-member": "Add member", "label.add-member": "Mitglied hinzufügen",
"label.add-step": "Add step", "label.add-step": "Schritt hinzufügen",
"label.add-website": "Website hinzufügen", "label.add-website": "Website hinzufügen",
"label.admin": "Administrator", "label.admin": "Administrator",
"label.after": "Nach", "label.after": "Nach",
@ -16,7 +16,7 @@
"label.back": "Zurück", "label.back": "Zurück",
"label.before": "Vor", "label.before": "Vor",
"label.bounce-rate": "Absprungrate", "label.bounce-rate": "Absprungrate",
"label.breakdown": "Breakdown", "label.breakdown": "Aufschlüsselung",
"label.browser": "Browser", "label.browser": "Browser",
"label.browsers": "Browser", "label.browsers": "Browser",
"label.cancel": "Abbrechen", "label.cancel": "Abbrechen",
@ -24,21 +24,21 @@
"label.cities": "Städte", "label.cities": "Städte",
"label.city": "Stadt", "label.city": "Stadt",
"label.clear-all": "Alles löschen", "label.clear-all": "Alles löschen",
"label.compare": "Compare", "label.compare": "Vergleich",
"label.confirm": "Bestätigen", "label.confirm": "Bestätigen",
"label.confirm-password": "Passwort wiederholen", "label.confirm-password": "Passwort wiederholen",
"label.contains": "Enthält", "label.contains": "Enthält",
"label.continue": "Weiter", "label.continue": "Weiter",
"label.count": "Count", "label.count": "Anzahl",
"label.countries": "Länder", "label.countries": "Länder",
"label.country": "Land", "label.country": "Land",
"label.create": "Create", "label.create": "Erstellen",
"label.create-report": "Bericht erstellen", "label.create-report": "Bericht erstellen",
"label.create-team": "Team erstellen", "label.create-team": "Team erstellen",
"label.create-user": "Benutzer erstellen", "label.create-user": "Benutzer erstellen",
"label.created": "Erstellt", "label.created": "Erstellt",
"label.created-by": "Created By", "label.created-by": "Erstellt von",
"label.current": "Current", "label.current": "Aktuell",
"label.current-password": "Derzeitiges Passwort", "label.current-password": "Derzeitiges Passwort",
"label.custom-range": "Benutzerdefinierter Bereich", "label.custom-range": "Benutzerdefinierter Bereich",
"label.dashboard": "Übersicht", "label.dashboard": "Übersicht",
@ -48,7 +48,7 @@
"label.day": "Tag", "label.day": "Tag",
"label.default-date-range": "Voreingestellter Datumsbereich", "label.default-date-range": "Voreingestellter Datumsbereich",
"label.delete": "Löschen", "label.delete": "Löschen",
"label.delete-report": "Delete report", "label.delete-report": "Bericht löschen",
"label.delete-team": "Team löschen", "label.delete-team": "Team löschen",
"label.delete-user": "Benutzer löschen", "label.delete-user": "Benutzer löschen",
"label.delete-website": "Website löschen", "label.delete-website": "Website löschen",
@ -63,9 +63,9 @@
"label.dropoff": "Dropoff", "label.dropoff": "Dropoff",
"label.edit": "Bearbeiten", "label.edit": "Bearbeiten",
"label.edit-dashboard": "Dashboard bearbeiten", "label.edit-dashboard": "Dashboard bearbeiten",
"label.edit-member": "Edit member", "label.edit-member": "Mitglied bearbeiten",
"label.enable-share-url": "Freigabe-URL aktivieren", "label.enable-share-url": "Freigabe-URL aktivieren",
"label.end-step": "End Step", "label.end-step": "Schritt beenden",
"label.entry": "Entry URL", "label.entry": "Entry URL",
"label.event": "Event", "label.event": "Event",
"label.event-data": "Eventdaten", "label.event-data": "Eventdaten",
@ -79,16 +79,16 @@
"label.filter-raw": "Rohdaten", "label.filter-raw": "Rohdaten",
"label.filters": "Filter", "label.filters": "Filter",
"label.funnel": "Funnel", "label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.funnel-description": "Verstehe die Konversions- und Dropoffrate von Nutzern.",
"label.goal": "Goal", "label.goal": "Ziel",
"label.goals": "Goals", "label.goals": "Ziele",
"label.goals-description": "Track your goals for pageviews and events.", "label.goals-description": "Verfolgen Sie Ihre Ziele für Aufrufe und Events.",
"label.greater-than": "Größer als", "label.greater-than": "Größer als",
"label.greater-than-equals": "Größer oder gleich", "label.greater-than-equals": "Größer oder gleich",
"label.host": "Host", "label.host": "Host",
"label.hosts": "Hosts", "label.hosts": "Hosts",
"label.insights": "Insights", "label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.", "label.insights-description": "Tauchen Sie tiefer in Ihre Daten mit Filtern und Segmenten ein.",
"label.is": "Ist", "label.is": "Ist",
"label.is-not": "Ist nicht", "label.is-not": "Ist nicht",
"label.is-not-set": "Ist nicht gesetzt", "label.is-not-set": "Ist nicht gesetzt",
@ -96,7 +96,7 @@
"label.join": "Beitreten", "label.join": "Beitreten",
"label.join-team": "Team beitreten", "label.join-team": "Team beitreten",
"label.journey": "Journey", "label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.", "label.journey-description": "Verstehen Sie, wie Nutzer Ihre Website navigieren.",
"label.language": "Sprache", "label.language": "Sprache",
"label.languages": "Sprachen", "label.languages": "Sprachen",
"label.laptop": "Laptop", "label.laptop": "Laptop",
@ -109,15 +109,15 @@
"label.less-than-equals": "Kleiner oder gleich", "label.less-than-equals": "Kleiner oder gleich",
"label.login": "Anmelden", "label.login": "Anmelden",
"label.logout": "Abmelden", "label.logout": "Abmelden",
"label.manage": "Manage", "label.manage": "Verwalten",
"label.manager": "Manager", "label.manager": "Manager",
"label.max": "Max", "label.max": "Max",
"label.member": "Member", "label.member": "Mitglied",
"label.members": "Mitglieder", "label.members": "Mitglieder",
"label.min": "Min", "label.min": "Min",
"label.mobile": "Handy", "label.mobile": "Handy",
"label.more": "Mehr", "label.more": "Mehr",
"label.my-account": "My account", "label.my-account": "Mein Konto",
"label.my-websites": "Meine Websites", "label.my-websites": "Meine Websites",
"label.name": "Name", "label.name": "Name",
"label.new-password": "Neues Passwort", "label.new-password": "Neues Passwort",
@ -133,11 +133,11 @@
"label.pages": "Seiten", "label.pages": "Seiten",
"label.password": "Passwort", "label.password": "Passwort",
"label.powered-by": "Betrieben durch {name}", "label.powered-by": "Betrieben durch {name}",
"label.previous": "Previous", "label.previous": "Vorherige",
"label.previous-period": "Previous period", "label.previous-period": "Vorheriger Zeitraum",
"label.previous-year": "Previous year", "label.previous-year": "Vorheriges Jahr",
"label.profile": "Profil", "label.profile": "Profil",
"label.property": "Property", "label.property": "Besitz",
"label.queries": "Abfragen", "label.queries": "Abfragen",
"label.query": "Abfrage", "label.query": "Abfrage",
"label.query-parameters": "Abfrageparameter", "label.query-parameters": "Abfrageparameter",
@ -149,33 +149,33 @@
"label.region": "Region", "label.region": "Region",
"label.regions": "Regionen", "label.regions": "Regionen",
"label.remove": "Entfernen", "label.remove": "Entfernen",
"label.remove-member": "Remove member", "label.remove-member": "Mitglied entfernen",
"label.reports": "Berichte", "label.reports": "Berichte",
"label.required": "Erforderlich", "label.required": "Erforderlich",
"label.reset": "Zurücksetzen", "label.reset": "Zurücksetzen",
"label.reset-website": "Statistik zurücksetzen", "label.reset-website": "Statistik zurücksetzen",
"label.retention": "Retention", "label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.retention-description": "Messen Sie die Presenz Ihrer Website, indem Sie tracken wie oft Nutzer zurückkehren.",
"label.role": "Rolle", "label.role": "Rolle",
"label.run-query": "Abfrage starten", "label.run-query": "Abfrage starten",
"label.save": "Speichern", "label.save": "Speichern",
"label.screens": "Bildschirmauflösungen", "label.screens": "Bildschirmauflösungen",
"label.search": "Search", "label.search": "Suche",
"label.select": "Select", "label.select": "Auswählen",
"label.select-date": "Datum auswählen", "label.select-date": "Datum auswählen",
"label.select-role": "Select role", "label.select-role": "Rolle auswählen",
"label.select-website": "Website auswählen", "label.select-website": "Website auswählen",
"label.sessions": "Sitzungen", "label.sessions": "Sitzungen",
"label.settings": "Einstellungen", "label.settings": "Einstellungen",
"label.share-url": "Freigabe-URL", "label.share-url": "Freigabe-URL",
"label.single-day": "Ein Tag", "label.single-day": "Ein Tag",
"label.start-step": "Start Step", "label.start-step": "Schritt starten",
"label.steps": "Steps", "label.steps": "Schritte",
"label.sum": "Summe", "label.sum": "Summe",
"label.tablet": "Tablet", "label.tablet": "Tablet",
"label.team": "Team", "label.team": "Team",
"label.team-id": "Team-ID", "label.team-id": "Team-ID",
"label.team-manager": "Team manager", "label.team-manager": "Team-Manager",
"label.team-member": "Team-Mitglied", "label.team-member": "Team-Mitglied",
"label.team-name": "Name des Teams", "label.team-name": "Name des Teams",
"label.team-owner": "Team-Eigentümer", "label.team-owner": "Team-Eigentümer",
@ -193,8 +193,8 @@
"label.total": "Gesamt", "label.total": "Gesamt",
"label.total-records": "Datensätze insgesamt", "label.total-records": "Datensätze insgesamt",
"label.tracking-code": "Tracking Code", "label.tracking-code": "Tracking Code",
"label.transfer": "Transfer", "label.transfer": "Übertragung",
"label.transfer-website": "Transfer website", "label.transfer-website": "Website übertragen",
"label.true": "Wahr", "label.true": "Wahr",
"label.type": "Typ", "label.type": "Typ",
"label.unique": "Eindeutig", "label.unique": "Eindeutig",
@ -208,29 +208,29 @@
"label.username": "Benutzername", "label.username": "Benutzername",
"label.users": "Benutzer", "label.users": "Benutzer",
"label.utm": "UTM", "label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.", "label.utm-description": "Tracken Sie Ihre Kampagnen mit Hilfe von UTM Parametern.",
"label.value": "Wert", "label.value": "Wert",
"label.view": "Anzeigen", "label.view": "Anzeigen",
"label.view-details": "Details anzeigen", "label.view-details": "Details anzeigen",
"label.view-only": "Nur ansehen", "label.view-only": "Nur ansehen",
"label.views": "Aufrufe", "label.views": "Aufrufe",
"label.views-per-visit": "Views per visit", "label.views-per-visit": "Aufrufe pro Besuch",
"label.visit-duration": "Durchschn. Besuchszeit", "label.visit-duration": "Durchschn. Besuchszeit",
"label.visitors": "Besucher", "label.visitors": "Besucher",
"label.visits": "Visits", "label.visits": "Besuche",
"label.website": "Website", "label.website": "Website",
"label.website-id": "Website ID", "label.website-id": "Website ID",
"label.websites": "Websites", "label.websites": "Websites",
"label.window": "Fenster", "label.window": "Fenster",
"label.yesterday": "Gestern", "label.yesterday": "Gestern",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.", "message.action-confirmation": "Tippen Sie {confirmation} in das untenliegende Feld, um zu bestätigen.",
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}", "message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
"message.collected-data": "Collected data", "message.collected-data": "Gesammelte Daten",
"message.confirm-delete": "Sind Sie sich sicher, {target} zu löschen?", "message.confirm-delete": "Sind Sie sich sicher, {target} zu löschen?",
"message.confirm-leave": "Sind Sie sicher, dass die {target} verlassen möchten?", "message.confirm-leave": "Sind Sie sicher, dass die {target} verlassen möchten?",
"message.confirm-remove": "Are you sure you want to remove {target}?", "message.confirm-remove": "Sind Sie sicher, dass Sie {target} entfernen möchten?",
"message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?", "message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?",
"message.delete-team-warning": "Deleting a team will also delete all team websites.", "message.delete-team-warning": "Alle zugehörigen Websiten werden ebenfalls gelöscht.",
"message.delete-website-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.", "message.delete-website-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.",
"message.error": "Es ist ein Fehler aufgetreten.", "message.error": "Es ist ein Fehler aufgetreten.",
"message.event-log": "{event} auf {url}", "message.event-log": "{event} auf {url}",
@ -256,12 +256,12 @@
"message.team-not-found": "Team nicht gefunden.", "message.team-not-found": "Team nicht gefunden.",
"message.team-websites-info": "Websites können von jedem im Team eingesehen werden.", "message.team-websites-info": "Websites können von jedem im Team eingesehen werden.",
"message.tracking-code": "Tracking Code", "message.tracking-code": "Tracking Code",
"message.transfer-team-website-to-user": "Transfer this website to your account?", "message.transfer-team-website-to-user": "Möchten Sie diese Website auf Ihr Konto übertragen?",
"message.transfer-user-website-to-team": "Select the team to transfer this website to.", "message.transfer-user-website-to-team": "Wählen Sie das Team, auf das die Website übertragen wird.",
"message.transfer-website": "Transfer website ownership to your account or another team.", "message.transfer-website": "Übertragen Sie den Besitz der Website auf Ihren Account oder ein anderes Team.",
"message.triggered-event": "Triggered event", "message.triggered-event": "Event ausgelöst",
"message.user-deleted": "Benutzer gelöscht.", "message.user-deleted": "Benutzer gelöscht.",
"message.viewed-page": "Viewed page", "message.viewed-page": "Seite besucht",
"message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}", "message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}",
"message.visitors-dropped-off": "Visitors dropped off" "message.visitors-dropped-off": "Besucher haben die Seite verlassen"
} }

View File

@ -1,267 +1,267 @@
{ {
"label.access-code": "Access code", "label.access-code": "Access code",
"label.actions": "សកម្មភាព", "label.actions": "សកម្មភាព",
"label.activity-log": "Activity log", "label.activity-log": "កំណត់ហេតុ​សកម្មភាព",
"label.add": "Add", "label.add": "បង្កើតបន្ថែម",
"label.add-description": "Add description", "label.add-description": "បន្ថែមពិពណ៌នា",
"label.add-member": "Add member", "label.add-member": "បន្ថែមសមាជិក",
"label.add-step": "Add step", "label.add-step": "បន្ថែមជំហាន",
"label.add-website": "បន្ថែមគេហទំព័រ", "label.add-website": "បន្ថែមគេហទំព័រ",
"label.admin": "អ្នកគ្រប់គ្រង", "label.admin": "អ្នកគ្រប់គ្រង",
"label.after": "After", "label.after": "បន្ទាប់",
"label.all": "ទាំងអស់", "label.all": "ទាំងអស់",
"label.all-time": "គ្រប់ពេល", "label.all-time": "គ្រប់ពេល",
"label.analytics": "Analytics", "label.analytics": "Analytics",
"label.average": "Average", "label.average": "ជាមធ្យម",
"label.back": "ថយក្រោយ", "label.back": "ថយក្រោយ",
"label.before": "Before", "label.before": "មុន",
"label.bounce-rate": "ចំនួនវិលត្រឡប់", "label.bounce-rate": "ចំនួនវិលត្រឡប់",
"label.breakdown": "Breakdown", "label.breakdown": "Breakdown",
"label.browser": "Browser", "label.browser": "Browser",
"label.browsers": "កម្មវិធី", "label.browsers": "កម្មវិធី",
"label.cancel": "បោះបង់", "label.cancel": "បោះបង់",
"label.change-password": "ផ្លាស់ប្តូរពាក្យសម្ងាត់", "label.change-password": "ផ្លាស់ប្តូរពាក្យសម្ងាត់",
"label.cities": "Cities", "label.cities": "ទីក្រុង",
"label.city": "City", "label.city": "ទីក្រុង",
"label.clear-all": "Clear all", "label.clear-all": "លុបទាំងអស់",
"label.compare": "Compare", "label.compare": "ប្រៀបធៀប",
"label.confirm": "Confirm", "label.confirm": "បញ្ជាក់",
"label.confirm-password": "បញ្ជាក់ពាក្យសម្ងាត់", "label.confirm-password": "បញ្ជាក់ពាក្យសម្ងាត់",
"label.contains": "Contains", "label.contains": "មាន",
"label.continue": "Continue", "label.continue": "បន្ត",
"label.count": "Count", "label.count": "ចំនួន",
"label.countries": "ប្រទេស", "label.countries": "ប្រទេស",
"label.country": "Country", "label.country": "ប្រទេស",
"label.create": "Create", "label.create": "បង្កើត",
"label.create-report": "Create report", "label.create-report": "បង្កើតរបាយការណ៍",
"label.create-team": "Create team", "label.create-team": "បង្កើតក្រុម",
"label.create-user": "Create user", "label.create-user": "បង្កើតអ្នកប្រើប្រាស់",
"label.created": "Created", "label.created": "បង្កើតនៅ",
"label.created-by": "Created By", "label.created-by": "បង្កើតដោយ",
"label.current": "Current", "label.current": "បច្ចុប្បន្ន",
"label.current-password": "ពាក្យសម្ងាត់បច្ចុប្បន្ន", "label.current-password": "ពាក្យសម្ងាត់បច្ចុប្បន្ន",
"label.custom-range": "កំណត់ដោយខ្លួនឯង", "label.custom-range": "កំណត់ដោយខ្លួនឯង",
"label.dashboard": "ផ្ទាំងគ្រប់គ្រង", "label.dashboard": "ផ្ទាំងគ្រប់គ្រង",
"label.data": "Data", "label.data": "ទិន្នន័យ",
"label.date": "Date", "label.date": "កាលបរិច្ឆេទ",
"label.date-range": "ចន្លោះកាលបរិច្ឆេទ", "label.date-range": "ចន្លោះកាលបរិច្ឆេទ",
"label.day": "Day", "label.day": "ថ្ងៃ",
"label.default-date-range": "ចន្លោះកាលបរិច្ឆេទស្រាប់", "label.default-date-range": "ចន្លោះកាលបរិច្ឆេទដើម",
"label.delete": "លុប", "label.delete": "លុប",
"label.delete-report": "Delete report", "label.delete-report": "លុបរបាយការណ៍",
"label.delete-team": "Delete team", "label.delete-team": "លុបក្រុម",
"label.delete-user": "Delete user", "label.delete-user": "លុបអ្នកប្រើប្រាស់",
"label.delete-website": "លុបគេហទំព័រ", "label.delete-website": "លុបគេហទំព័រ",
"label.description": "Description", "label.description": "ការពិពណ៌នា",
"label.desktop": "កុំព្យូទ័រលើតុ", "label.desktop": "កុំព្យូទ័រលើតុ",
"label.details": "Details", "label.details": "ព័ត៌មានលម្អិត",
"label.device": "Device", "label.device": "ឧបករណ៍",
"label.devices": "ឧបករណ៍", "label.devices": "ឧបករណ៍",
"label.dismiss": "បដិសេធ", "label.dismiss": "រំសាយ",
"label.does-not-contain": "Does not contain", "label.does-not-contain": "មិនមាន",
"label.domain": "ឈ្មោះគេហទំព័រ", "label.domain": "Domain",
"label.dropoff": "Dropoff", "label.dropoff": "Dropoff",
"label.edit": "កែប្រែ", "label.edit": "កែប្រែ",
"label.edit-dashboard": "កែផ្ទាំងគ្រប់គ្រង", "label.edit-dashboard": "កែផ្ទាំងគ្រប់គ្រង",
"label.edit-member": "Edit member", "label.edit-member": "កែព័ត៌មានសមាជិក",
"label.enable-share-url": "បើកការចែករំលែក URL", "label.enable-share-url": "បើកការចែករំលែក URL",
"label.end-step": "End Step", "label.end-step": "បញ្ចប់ជំហាន",
"label.entry": "Entry URL", "label.entry": "URL ចូល",
"label.event": "Event", "label.event": "ព្រឹត្តិការណ៍",
"label.event-data": "Event data", "label.event-data": "ទិន្នន័យព្រឹត្តិការណ៍",
"label.events": "ព្រឹត្តិការណ៍", "label.events": "ព្រឹត្តិការណ៍",
"label.exit": "Exit URL", "label.exit": "URL ចេញ",
"label.false": "False", "label.false": "មិនពិត",
"label.field": "Field", "label.field": "Field",
"label.fields": "Fields", "label.fields": "Fields",
"label.filter": "Filter", "label.filter": "ចម្រោះ",
"label.filter-combined": "រួមបញ្ចូលគ្នា", "label.filter-combined": "រួមបញ្ចូលគ្នា",
"label.filter-raw": "ដើម", "label.filter-raw": "ដើម",
"label.filters": "Filters", "label.filters": "ចម្រោះ",
"label.funnel": "Funnel", "label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.", "label.funnel-description": "Understand the conversion and drop-off rate of users.",
"label.goal": "Goal", "label.goal": "គោលដៅ",
"label.goals": "Goals", "label.goals": "គោលដៅ",
"label.goals-description": "Track your goals for pageviews and events.", "label.goals-description": "តាមដានគោលដៅរបស់អ្នកសម្រាប់ pageviews និង events។",
"label.greater-than": "Greater than", "label.greater-than": "ធំជាង",
"label.greater-than-equals": "Greater than or equals", "label.greater-than-equals": "ធំជាងឬស្មើ",
"label.host": "Host", "label.host": "Host",
"label.hosts": "Hosts", "label.hosts": "Hosts",
"label.insights": "Insights", "label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.", "label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is", "label.is": "គឺ",
"label.is-not": "Is not", "label.is-not": "មិនមែន",
"label.is-not-set": "Is not set", "label.is-not-set": "មិនបានកំណត់",
"label.is-set": "Is set", "label.is-set": "បានកំណត់",
"label.join": "Join", "label.join": "ចូលរួម",
"label.join-team": "Join team", "label.join-team": "ចូលក្រុម",
"label.journey": "Journey", "label.journey": "​ដំណើរ",
"label.journey-description": "Understand how users navigate through your website.", "label.journey-description": "ស្វែងយល់ពីការប្រើប្រាស់គេហទំព័ររបស់អតិថិជនអ្នក។",
"label.language": "ភាសា", "label.language": "ភាសា",
"label.languages": "ភាសា", "label.languages": "ភាសា",
"label.laptop": "កុំព្យូទ័រយួរដៃ", "label.laptop": "កុំព្យូទ័រយួរដៃ",
"label.last-days": "{x} ថ្ងៃមុន", "label.last-days": "{x} ថ្ងៃចុងក្រោយ",
"label.last-hours": "{x} ម៉ោងមុន", "label.last-hours": "{x} ម៉ោងចុងក្រោយ",
"label.last-months": "Last {x} months", "label.last-months": "{x} ខែចុងក្រោយ",
"label.leave": "Leave", "label.leave": "ចាកចេញ",
"label.leave-team": "Leave team", "label.leave-team": "ចេញពីក្រុម",
"label.less-than": "Less than", "label.less-than": "តិច​ជាង",
"label.less-than-equals": "Less than or equals", "label.less-than-equals": "តិចជាង ឬស្មើ",
"label.login": "Login", "label.login": "Login",
"label.logout": "ចេញ", "label.logout": "Logout",
"label.manage": "Manage", "label.manage": "គ្រប់គ្រង",
"label.manager": "Manager", "label.manager": "អ្នកគ្រប់គ្រង",
"label.max": "Max", "label.max": "Max",
"label.member": "Member", "label.member": "សមាជិក",
"label.members": "Members", "label.members": "សមាជិក",
"label.min": "Min", "label.min": "Min",
"label.mobile": "ទូរស័ព្ទចល័ត", "label.mobile": "ទូរស័ព្ទចល័ត",
"label.more": "បន្ថែម", "label.more": "បន្ថែម",
"label.my-account": "My account", "label.my-account": "គណនី​របស់ខ្ញុំ",
"label.my-websites": "My websites", "label.my-websites": "គេហទំព័ររបស់ខ្ញុំ",
"label.name": "ឈ្មោះ", "label.name": "ឈ្មោះ",
"label.new-password": "ពាក្យសម្ងាត់​ថ្មី", "label.new-password": "ពាក្យសម្ងាត់​ថ្មី",
"label.none": "មិនមាន", "label.none": "មិនមាន",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}", "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK", "label.ok": "OK",
"label.os": "OS", "label.os": "OS",
"label.overview": "Overview", "label.overview": "ទិដ្ឋភាពរួម",
"label.owner": "ម្ចាស់", "label.owner": "ម្ចាស់",
"label.page-of": "Page {current} of {total}", "label.page-of": "ទំព័រទី {current} នៃ {total}",
"label.page-views": "អ្នកមើលទំព័រ", "label.page-views": "អ្នកមើលទំព័រ",
"label.pageTitle": "Page title", "label.pageTitle": "ចំណងជើងទំព័រ",
"label.pages": "ទំព័រ", "label.pages": "ទំព័រ",
"label.password": "ពាក្យសម្ងាត់​", "label.password": "ពាក្យសម្ងាត់​",
"label.powered-by": "ដំណើរការដោយ {name}", "label.powered-by": "ដំណើរការដោយ {name}",
"label.previous": "Previous", "label.previous": "មុន",
"label.previous-period": "Previous period", "label.previous-period": "មួយរយៈពេលមុន",
"label.previous-year": "Previous year", "label.previous-year": "ឆ្នាំ​មុន",
"label.profile": "ប្រវត្តិរូប", "label.profile": "គណនី",
"label.property": "Property", "label.property": "Property",
"label.queries": "Queries", "label.queries": "Queries",
"label.query": "Query", "label.query": "Query",
"label.query-parameters": "ប៉ារ៉ាម៉ែត្រ Query", "label.query-parameters": "ប៉ារ៉ាម៉ែត្រ Query",
"label.realtime": "ឥលូវនេះ", "label.realtime": "ឥលូវនេះ",
"label.referrer": "Referrer", "label.referrer": "អ្នកណែនាំ",
"label.referrers": "អ្នកណែនាំ", "label.referrers": "អ្នកណែនាំ",
"label.refresh": "ផ្ទុកឡើងវិញ", "label.refresh": "ផ្ទុកឡើងវិញ",
"label.regenerate": "Regenerate", "label.regenerate": "Regenerate",
"label.region": "Region", "label.region": "តំបន់",
"label.regions": "Regions", "label.regions": "តំបន់",
"label.remove": "Remove", "label.remove": "លុប",
"label.remove-member": "Remove member", "label.remove-member": "លុបសមាជិកក្រុម",
"label.reports": "Reports", "label.reports": "របាយការណ៍",
"label.required": "ទាមទារ", "label.required": "ទាមទារ",
"label.reset": "កំណត់ឡើងវិញ", "label.reset": "កែសម្រួល",
"label.reset-website": "កំណត់ស្ថិតិឡើងវិញ", "label.reset-website": "កំណត់ស្ថិតិឡើងវិញ",
"label.retention": "Retention", "label.retention": "ការរក្សាទុក",
"label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.role": "Role", "label.role": "មុខងារ",
"label.run-query": "Run query", "label.run-query": "Run query",
"label.save": "រក្សាទុក", "label.save": "រក្សាទុក",
"label.screens": "ប្រភេទឧបករណ៍", "label.screens": "ប្រភេទឧបករណ៍",
"label.search": "Search", "label.search": "ស្វែងរក",
"label.select": "Select", "label.select": "ជ្រើសរើស",
"label.select-date": "Select date", "label.select-date": "ជ្រើសរើសកាលបរិច្ឆេទ",
"label.select-role": "Select role", "label.select-role": "ជ្រើសរើសមុខងារ",
"label.select-website": "Select website", "label.select-website": "ជ្រើសរើសគេហទំព័រ",
"label.sessions": "Sessions", "label.sessions": "Sessions",
"label.settings": "ការកំណត់", "label.settings": "ការកំណត់",
"label.share-url": "ចែករំលែក URL", "label.share-url": "ចែករំលែក URL",
"label.single-day": "ថ្ងៃតែមួយ", "label.single-day": "ថ្ងៃតែមួយ",
"label.start-step": "Start Step", "label.start-step": "ជំហានចាប់ផ្តើម",
"label.steps": "Steps", "label.steps": "ជំហាន",
"label.sum": "Sum", "label.sum": "Sum",
"label.tablet": "ថេប្លេត", "label.tablet": "ថេប្លេត",
"label.team": "Team", "label.team": "ក្រុម",
"label.team-id": "Team ID", "label.team-id": "ID ក្រុម",
"label.team-manager": "Team manager", "label.team-manager": "អ្នកគ្រប់គ្រងក្រុម",
"label.team-member": "Team member", "label.team-member": "សមាជិកក្រុម",
"label.team-name": "Team name", "label.team-name": "ឈ្មោះក្រុម",
"label.team-owner": "Team owner", "label.team-owner": "ម្ចាស់ក្រុម",
"label.team-view-only": "Team view only", "label.team-view-only": "Team view only",
"label.team-websites": "Team websites", "label.team-websites": "គេហទំព័ររបស់ក្រុម",
"label.teams": "Teams", "label.teams": "ក្រុម",
"label.theme": "រូបរាង", "label.theme": "រូបរាង",
"label.this-month": "ខែនេះ", "label.this-month": "ខែនេះ",
"label.this-week": "ស​ប្តា​ហ៍​នេះ", "label.this-week": "ស​ប្តា​ហ៍​នេះ",
"label.this-year": "ឆ្នាំ​នេះ", "label.this-year": "ឆ្នាំ​នេះ",
"label.timezone": "តំបន់ម៉ោង", "label.timezone": "តំបន់ម៉ោង",
"label.title": "Title", "label.title": "ចំណងជើង",
"label.today": "ថ្ងៃនេះ", "label.today": "ថ្ងៃនេះ",
"label.toggle-charts": "បិទ/បើកតារាង", "label.toggle-charts": "បិទ/បើកតារាង",
"label.total": "Total", "label.total": "សរុប",
"label.total-records": "Total records", "label.total-records": "កំណត់ត្រាសរុប",
"label.tracking-code": "លេខកូដតាមដាន", "label.tracking-code": "លេខកូដតាមដាន",
"label.transfer": "Transfer", "label.transfer": "ការផ្ទេរ",
"label.transfer-website": "Transfer website", "label.transfer-website": "ការផ្ទេរគេហទំព័រ",
"label.true": "True", "label.true": "ពិត",
"label.type": "Type", "label.type": "Type",
"label.unique": "Unique", "label.unique": "Unique",
"label.unique-visitors": "អ្នកចូលមើលម្នាក់ៗ", "label.unique-visitors": "អ្នកចូលមើលម្នាក់ៗ",
"label.unknown": "មិនស្គាល់", "label.unknown": "មិនស្គាល់",
"label.untitled": "Untitled", "label.untitled": "គ្មានចំណងជើង",
"label.update": "Update", "label.update": "Update",
"label.url": "URL", "label.url": "URL",
"label.urls": "URLs", "label.urls": "URLs",
"label.user": "User", "label.user": "អ្នកប្រើប្រាស់",
"label.username": "ឈ្មោះ​អ្នកប្រើប្រាស់", "label.username": "ឈ្មោះ​អ្នកប្រើប្រាស់",
"label.users": "Users", "label.users": "អ្នកប្រើប្រាស់",
"label.utm": "UTM", "label.utm": "UTM",
"label.utm-description": "Track your campaigns through UTM parameters.", "label.utm-description": "តាមដានយុទ្ធនាការរបស់អ្នកតាមរយៈប៉ារ៉ាម៉ែត្រ UTM។",
"label.value": "Value", "label.value": "Value",
"label.view": "View", "label.view": "View",
"label.view-details": "មើលព័ត៌មានលម្អិត", "label.view-details": "មើលព័ត៌មានលម្អិត",
"label.view-only": "View only", "label.view-only": "បានតែមើលប៉ុណ្ណោះ",
"label.views": "អ្នកចូលមើល", "label.views": "អ្នកចូលមើល",
"label.views-per-visit": "Views per visit", "label.views-per-visit": "Views per visit",
"label.visit-duration": "មើលជាមធ្យម", "label.visit-duration": "រយៈពេលទស្សនា",
"label.visitors": "អ្នកទស្សនា", "label.visitors": "អ្នកទស្សនា",
"label.visits": "Visits", "label.visits": "ទស្សនា",
"label.website": "Website", "label.website": "គេហទំព័រ",
"label.website-id": "Website ID", "label.website-id": "ID គេហទំព័រ",
"label.websites": "គេហទំព័រ", "label.websites": "គេហទំព័រ",
"label.window": "Window", "label.window": "Window",
"label.yesterday": "ម្សិលមិញ", "label.yesterday": "ម្សិលមិញ",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.", "message.action-confirmation": "សសេរ {confirmation} នៅក្នុងប្រអប់ខាងក្រោមដើម្បីបញ្ជាក់។",
"message.active-users": "មានអ្នកមើល {x} នាក់ ឥលូវនេះ", "message.active-users": "មានអ្នកមើល {x} នាក់ ឥលូវនេះ",
"message.collected-data": "Collected data", "message.collected-data": "ទិន្នន័យដែលបានប្រមូលទុក",
"message.confirm-delete": "តើអ្នកប្រាកដថាចង់លុប {target} ទេ?", "message.confirm-delete": "តើអ្នកប្រាកដថាចង់លុប {target} ទេ?",
"message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-leave": "តើអ្នកប្រាកដថាចង់ចាកចេញ {target} ទេ?",
"message.confirm-remove": "Are you sure you want to remove {target}?", "message.confirm-remove": "តើអ្នកប្រាកដថាចង់លុប {target} ទេ?",
"message.confirm-reset": "តើអ្នកប្រាកដថាចង់កំណត់ស្ថិតិរបស់ {target} ឡើងវិញទេ?", "message.confirm-reset": "តើអ្នកប្រាកដថាចង់កំណត់ស្ថិតិរបស់ {target} ឡើងវិញទេ?",
"message.delete-team-warning": "Deleting a team will also delete all team websites.", "message.delete-team-warning": "ពេលលុបក្រុម គេហទំព័ររបស់ក្រុមក៏នឹងត្រូវលប់ចោលទាំងអស់ផងដែរ។",
"message.delete-website-warning": "ទិន្នន័យដែលពាក់ព័ន្ធទាំងអស់នឹងត្រូវបានលុបផងដែរ។", "message.delete-website-warning": "ទិន្នន័យរបស់គេហទំព័រទាំងអស់នឹងត្រូវលុបចោល។",
"message.error": "មាន​អ្វីមួយ​មិន​ប្រក្រតី។", "message.error": "មាន​អ្វីមួយ​មិន​ប្រក្រតី។",
"message.event-log": "{event} on {url}", "message.event-log": "{event} on {url}",
"message.go-to-settings": "ការកំណត់", "message.go-to-settings": "ការកំណត់",
"message.incorrect-username-password": "ឈ្មោះអ្នកប្រើ/ពាក្យសម្ងាត់មិនត្រឹមត្រូវ។", "message.incorrect-username-password": "ឈ្មោះអ្នកប្រើពាក្យសម្ងាត់មិនត្រឹមត្រូវ។",
"message.invalid-domain": "ឈ្មោះគេហទំព័រមិន​ត្រឹមត្រូវ", "message.invalid-domain": "Domain មិន​ត្រឹមត្រូវ",
"message.min-password-length": "Minimum length of {n} characters", "message.min-password-length": "តិចបំផុតដែលមានអក្សរ {n} តួអក្សរ",
"message.new-version-available": "A new version of Umami {version} is available!", "message.new-version-available": "Version ថ្មីនៃ Umami {version} អាចប្រើប្រាស់បានហើយ!",
"message.no-data-available": "មិនមានទិន្នន័យទេ។", "message.no-data-available": "មិនមានទិន្នន័យ។",
"message.no-event-data": "No event data is available.", "message.no-event-data": "មិនមានទិន្នន័យព្រឹត្តិការណ៍ទេ។",
"message.no-match-password": "ពាក្យសម្ងាត់មិនត្រូវគ្នាទេ", "message.no-match-password": "ពាក្យសម្ងាត់មិនត្រូវគ្នាទេ",
"message.no-results-found": "No results were found.", "message.no-results-found": "មិនមានលទ្ធផល។",
"message.no-team-websites": "This team does not have any websites.", "message.no-team-websites": "ក្រុមនេះមិនមានគេហទំព័រទេ។",
"message.no-teams": "You have not created any teams.", "message.no-teams": "អ្នកមិនទាន់បានបង្កើតក្រុមណាមួយទេ។",
"message.no-users": "There are no users.", "message.no-users": "មិនមានអ្នកប្រើប្រាស់ទេ។",
"message.no-websites-configured": "អ្នកមិនទាន់បានដាក់គេហទំព័រណាមួយចូលទេ។", "message.no-websites-configured": "អ្នកមិនទាន់បានដាក់គេហទំព័រណាមួយចូលទេ។",
"message.page-not-found": "រកមិនឃើញទំព័រ។", "message.page-not-found": "រកមិនឃើញទំព័រ។",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", "message.reset-website": "ដើម្បីកែគេហទំព័រនេះឡើងវិញ សូមសរសេរ {confirmation} នៅក្នុងប្រអប់ខាងក្រោមដើម្បីបញ្ជាក់។",
"message.reset-website-warning": "ស្ថិតិទាំងអស់សម្រាប់គេហទំព័រនេះនឹងត្រូវបានលុប ប៉ុន្តែកូដតាមដានរបស់អ្នកនឹងនៅដដែល។", "message.reset-website-warning": "ស្ថិតិទាំងអស់សម្រាប់គេហទំព័រនេះនឹងត្រូវបានលុប ប៉ុន្តែកូដតាមដានរបស់អ្នកនឹងនៅដដែល។",
"message.saved": "រក្សាទុកដោយជោគជ័យ។", "message.saved": "រក្សាទុកដោយជោគជ័យ។",
"message.share-url": "នេះគឺជា URL ដែលបានចែករំលែកជាសាធារណៈសម្រាប់ {target}។", "message.share-url": "នេះគឺជា URL ដែលអាចចែករំលែកជាសាធារណៈបានសម្រាប់ {target}។",
"message.team-already-member": "You are already a member of the team.", "message.team-already-member": "អ្នកគឺជាសមាជិកនៃក្រុមរួចហើយ។",
"message.team-not-found": "Team not found.", "message.team-not-found": "រកក្រុមមិនឃើញទេ។",
"message.team-websites-info": "Websites can be viewed by anyone on the team.", "message.team-websites-info": "គេហទំព័រនេះអាចមើលបានតែសមាជិកក្រុមតែប៉ុណ្ណោះ",
"message.tracking-code": "លេខកូដតាមដាន", "message.tracking-code": "ដើម្បីតាមដានស្ថិតិសម្រាប់គេហទំព័រអ្នក សូមដាក់កូដខាងក្រោមទៅក្នុងផ្នែក <head>...</head> នៃ HTML របស់អ្នក។",
"message.transfer-team-website-to-user": "Transfer this website to your account?", "message.transfer-team-website-to-user": "ផ្ទេរគេហទំព័រនេះទៅគណនីរបស់អ្នក។?",
"message.transfer-user-website-to-team": "Select the team to transfer this website to.", "message.transfer-user-website-to-team": "ជ្រើសក្រុមដែរត្រូវផ្ទេរគេហទំព័រនេះទៅ។",
"message.transfer-website": "Transfer website ownership to your account or another team.", "message.transfer-website": "ផ្ទេរកម្មសិទ្ធិគេហទំព័រទៅគណនីរបស់អ្នក ឬក្រុមផ្សេងទៀត។",
"message.triggered-event": "Triggered event", "message.triggered-event": "Triggered event",
"message.user-deleted": "User deleted.", "message.user-deleted": "អ្នកប្រើប្រាស់ត្រូវបានលុបចោល។",
"message.viewed-page": "Viewed page", "message.viewed-page": "ទំព័រដែលបានមើល",
"message.visitor-log": "អ្នកមើលពីប្រទេស {country} ប្រើប្រាស់កម្មវិធី {browser} លើឧបករណ៍ {os} {device}", "message.visitor-log": "អ្នកមើលពីប្រទេស {country} ប្រើប្រាស់កម្មវិធី {browser} លើឧបករណ៍ {os} {device}",
"message.visitors-dropped-off": "Visitors dropped off" "message.visitors-dropped-off": "ចំនួនអ្នកទស្សនាធ្លាក់ចុះ"
} }

View File

@ -24,12 +24,12 @@
"label.cities": "Miasta", "label.cities": "Miasta",
"label.city": "Miasto", "label.city": "Miasto",
"label.clear-all": "Wyczyść wszystko", "label.clear-all": "Wyczyść wszystko",
"label.compare": "Compare", "label.compare": "Porównaj",
"label.confirm": "Potwierdź", "label.confirm": "Potwierdź",
"label.confirm-password": "Potwierdź hasło", "label.confirm-password": "Potwierdź hasło",
"label.contains": "Zawiera", "label.contains": "Zawiera",
"label.continue": "Kontynuuj", "label.continue": "Kontynuuj",
"label.count": "Count", "label.count": "Liczba",
"label.countries": "Kraje", "label.countries": "Kraje",
"label.country": "Państwo", "label.country": "Państwo",
"label.create": "Utwórz", "label.create": "Utwórz",
@ -38,7 +38,7 @@
"label.create-user": "Utwórz użytkownika", "label.create-user": "Utwórz użytkownika",
"label.created": "Utworzony", "label.created": "Utworzony",
"label.created-by": "Utworzony przez", "label.created-by": "Utworzony przez",
"label.current": "Current", "label.current": "Aktualny",
"label.current-password": "Aktualne hasło", "label.current-password": "Aktualne hasło",
"label.custom-range": "Zakres niestandardowy", "label.custom-range": "Zakres niestandardowy",
"label.dashboard": "Panel", "label.dashboard": "Panel",
@ -65,12 +65,12 @@
"label.edit-dashboard": "Edytuj panel", "label.edit-dashboard": "Edytuj panel",
"label.edit-member": "Edytuj członka", "label.edit-member": "Edytuj członka",
"label.enable-share-url": "Włącz udostępnianie adresu URL", "label.enable-share-url": "Włącz udostępnianie adresu URL",
"label.end-step": "End Step", "label.end-step": "Krok końcowy",
"label.entry": "Entry URL", "label.entry": "Entry URL",
"label.event": "Zdarzenie", "label.event": "Zdarzenie",
"label.event-data": "Dane zdarzenia", "label.event-data": "Dane zdarzenia",
"label.events": "Zdarzenia", "label.events": "Zdarzenia",
"label.exit": "Exit URL", "label.exit": "URL wyjściowy",
"label.false": "Fałsz", "label.false": "Fałsz",
"label.field": "Pole", "label.field": "Pole",
"label.fields": "Pola", "label.fields": "Pola",
@ -80,13 +80,13 @@
"label.filters": "Filtry", "label.filters": "Filtry",
"label.funnel": "Lejek", "label.funnel": "Lejek",
"label.funnel-description": "Zrozum wskaźniki konwersji i odpływu użytkowników.", "label.funnel-description": "Zrozum wskaźniki konwersji i odpływu użytkowników.",
"label.goal": "Goal", "label.goal": "Cel",
"label.goals": "Goals", "label.goals": "Cele",
"label.goals-description": "Track your goals for pageviews and events.", "label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Większe niż", "label.greater-than": "Większe niż",
"label.greater-than-equals": "Większe niż lub równe", "label.greater-than-equals": "Większe niż lub równe",
"label.host": "Host", "label.host": "Host",
"label.hosts": "Hosts", "label.hosts": "Hosty",
"label.insights": "Analiza", "label.insights": "Analiza",
"label.insights-description": "Poznaj lepiej swoje dane, korzystając z segmentów i filtrów.", "label.insights-description": "Poznaj lepiej swoje dane, korzystając z segmentów i filtrów.",
"label.is": "Równe", "label.is": "Równe",
@ -95,8 +95,8 @@
"label.is-set": "Ustawione", "label.is-set": "Ustawione",
"label.join": "Dołącz", "label.join": "Dołącz",
"label.join-team": "Dołącz do zespołu", "label.join-team": "Dołącz do zespołu",
"label.journey": "Journey", "label.journey": "Droga",
"label.journey-description": "Understand how users navigate through your website.", "label.journey-description": "Zrozum, w jaki sposób użytkownicy poruszają się po Twojej witrynie.",
"label.language": "Język", "label.language": "Język",
"label.languages": "Języki", "label.languages": "Języki",
"label.laptop": "Laptop", "label.laptop": "Laptop",
@ -133,23 +133,23 @@
"label.pages": "Strony", "label.pages": "Strony",
"label.password": "Hasło", "label.password": "Hasło",
"label.powered-by": "Obsługiwane przez {name}", "label.powered-by": "Obsługiwane przez {name}",
"label.previous": "Previous", "label.previous": "Poprzedni",
"label.previous-period": "Previous period", "label.previous-period": "Poprzedni okres",
"label.previous-year": "Previous year", "label.previous-year": "Poprzedni rok",
"label.profile": "Profil", "label.profile": "Profil",
"label.property": "Property", "label.property": "Property",
"label.queries": "Zapytania", "label.queries": "Zapytania",
"label.query": "Zapytanie", "label.query": "Zapytanie",
"label.query-parameters": "Parametry zapytania", "label.query-parameters": "Parametry zapytania",
"label.realtime": "Czas rzeczywisty", "label.realtime": "Czas rzeczywisty",
"label.referrer": "Referrer", "label.referrer": "Źródło odsyłające",
"label.referrers": "Źródła odsyłające", "label.referrers": "Źródła odsyłające",
"label.refresh": "Odśwież", "label.refresh": "Odśwież",
"label.regenerate": "Wygeneruj ponownie", "label.regenerate": "Wygeneruj ponownie",
"label.region": "Region", "label.region": "Region",
"label.regions": "Regiony", "label.regions": "Regiony",
"label.remove": "Usuń", "label.remove": "Usuń",
"label.remove-member": "Remove member", "label.remove-member": "Usuń członka",
"label.reports": "Raporty", "label.reports": "Raporty",
"label.required": "Wymagany", "label.required": "Wymagany",
"label.reset": "Zresetuj", "label.reset": "Zresetuj",
@ -169,7 +169,7 @@
"label.settings": "Ustawienia", "label.settings": "Ustawienia",
"label.share-url": "Udostępnij adres URL", "label.share-url": "Udostępnij adres URL",
"label.single-day": "W tym dniu", "label.single-day": "W tym dniu",
"label.start-step": "Start Step", "label.start-step": "Krok startowy",
"label.steps": "Kroki", "label.steps": "Kroki",
"label.sum": "Suma", "label.sum": "Suma",
"label.tablet": "Tablet", "label.tablet": "Tablet",
@ -217,7 +217,7 @@
"label.views-per-visit": "Widoków na wizytę", "label.views-per-visit": "Widoków na wizytę",
"label.visit-duration": "Średni czas wizyty", "label.visit-duration": "Średni czas wizyty",
"label.visitors": "Odwiedzający", "label.visitors": "Odwiedzający",
"label.visits": "Odwiedząjący", "label.visits": "Wizyty",
"label.website": "Witryna", "label.website": "Witryna",
"label.website-id": "ID witryny", "label.website-id": "ID witryny",
"label.websites": "Witryny", "label.websites": "Witryny",
@ -225,7 +225,7 @@
"label.yesterday": "Wczoraj", "label.yesterday": "Wczoraj",
"message.action-confirmation": "Wpisz {confirmation}, aby potwierdzić.", "message.action-confirmation": "Wpisz {confirmation}, aby potwierdzić.",
"message.active-users": "{x} aktualnie {x, plural, one {odwiedzający} other {odwiedzających}}", "message.active-users": "{x} aktualnie {x, plural, one {odwiedzający} other {odwiedzających}}",
"message.collected-data": "Collected data", "message.collected-data": "Zebrane dane",
"message.confirm-delete": "Czy na pewno chcesz usunąć {target}?", "message.confirm-delete": "Czy na pewno chcesz usunąć {target}?",
"message.confirm-leave": "Czy na pewno chcesz opuścić {target}?", "message.confirm-leave": "Czy na pewno chcesz opuścić {target}?",
"message.confirm-remove": "Czy na pewno chcesz usunąć {target}?", "message.confirm-remove": "Czy na pewno chcesz usunąć {target}?",

View File

@ -24,12 +24,12 @@
"label.cities": "市/县", "label.cities": "市/县",
"label.city": "市/县", "label.city": "市/县",
"label.clear-all": "清除全部", "label.clear-all": "清除全部",
"label.compare": "Compare", "label.compare": "比较",
"label.confirm": "确认", "label.confirm": "确认",
"label.confirm-password": "确认密码", "label.confirm-password": "确认密码",
"label.contains": "包含", "label.contains": "包含",
"label.continue": "继续", "label.continue": "继续",
"label.count": "Count", "label.count": "统计",
"label.countries": "国家/地区", "label.countries": "国家/地区",
"label.country": "国家/地区", "label.country": "国家/地区",
"label.create": "创建", "label.create": "创建",
@ -38,7 +38,7 @@
"label.create-user": "创建用户", "label.create-user": "创建用户",
"label.created": "已创建", "label.created": "已创建",
"label.created-by": "创建者", "label.created-by": "创建者",
"label.current": "Current", "label.current": "目前",
"label.current-password": "目前密码", "label.current-password": "目前密码",
"label.custom-range": "自定义时间段", "label.custom-range": "自定义时间段",
"label.dashboard": "仪表板", "label.dashboard": "仪表板",
@ -65,12 +65,12 @@
"label.edit-dashboard": "编辑仪表板", "label.edit-dashboard": "编辑仪表板",
"label.edit-member": "编辑成员", "label.edit-member": "编辑成员",
"label.enable-share-url": "启用共享链接", "label.enable-share-url": "启用共享链接",
"label.end-step": "End Step", "label.end-step": "结束步骤",
"label.entry": "Entry URL", "label.entry": "入口 URL",
"label.event": "事件", "label.event": "事件",
"label.event-data": "事件数据", "label.event-data": "事件数据",
"label.events": "行为类别", "label.events": "行为类别",
"label.exit": "Exit URL", "label.exit": "退出 URL",
"label.false": "否", "label.false": "否",
"label.field": "字段", "label.field": "字段",
"label.fields": "字段", "label.fields": "字段",
@ -80,13 +80,13 @@
"label.filters": "筛选", "label.filters": "筛选",
"label.funnel": "分析", "label.funnel": "分析",
"label.funnel-description": "了解用户的转换率和退出率。", "label.funnel-description": "了解用户的转换率和退出率。",
"label.goal": "Goal", "label.goal": "目标",
"label.goals": "Goals", "label.goals": "目标",
"label.goals-description": "Track your goals for pageviews and events.", "label.goals-description": "跟踪页面浏览量和事件的目标。",
"label.greater-than": "大于", "label.greater-than": "大于",
"label.greater-than-equals": "大于或等于", "label.greater-than-equals": "大于或等于",
"label.host": "Host", "label.host": "主机",
"label.hosts": "Hosts", "label.hosts": "主机",
"label.insights": "见解", "label.insights": "见解",
"label.insights-description": "通过使用筛选器和划分时间段来更深入地研究数据。", "label.insights-description": "通过使用筛选器和划分时间段来更深入地研究数据。",
"label.is": "等于", "label.is": "等于",
@ -95,8 +95,8 @@
"label.is-set": "已设置", "label.is-set": "已设置",
"label.join": "加入", "label.join": "加入",
"label.join-team": "加入团队", "label.join-team": "加入团队",
"label.journey": "Journey", "label.journey": "用户浏览轨迹",
"label.journey-description": "Understand how users navigate through your website.", "label.journey-description": "了解用户如何浏览网站。",
"label.language": "语言", "label.language": "语言",
"label.languages": "语言", "label.languages": "语言",
"label.laptop": "笔记本", "label.laptop": "笔记本",
@ -110,7 +110,7 @@
"label.login": "登录", "label.login": "登录",
"label.logout": "退出", "label.logout": "退出",
"label.manage": "管理", "label.manage": "管理",
"label.manager": "Manager", "label.manager": "管理者",
"label.max": "最大", "label.max": "最大",
"label.member": "成员", "label.member": "成员",
"label.members": "成员", "label.members": "成员",
@ -123,7 +123,7 @@
"label.new-password": "新密码", "label.new-password": "新密码",
"label.none": "无", "label.none": "无",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}", "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK", "label.ok": "好的",
"label.os": "操作系统", "label.os": "操作系统",
"label.overview": "概览", "label.overview": "概览",
"label.owner": "所有者", "label.owner": "所有者",
@ -133,11 +133,11 @@
"label.pages": "网页", "label.pages": "网页",
"label.password": "密码", "label.password": "密码",
"label.powered-by": "由 {name} 提供支持", "label.powered-by": "由 {name} 提供支持",
"label.previous": "Previous", "label.previous": "先前",
"label.previous-period": "Previous period", "label.previous-period": "上一时期",
"label.previous-year": "Previous year", "label.previous-year": "上一年",
"label.profile": "个人资料", "label.profile": "个人资料",
"label.property": "Property", "label.property": "属性",
"label.queries": "查询", "label.queries": "查询",
"label.query": "查询", "label.query": "查询",
"label.query-parameters": "查询参数", "label.query-parameters": "查询参数",
@ -169,13 +169,13 @@
"label.settings": "设置", "label.settings": "设置",
"label.share-url": "共享链接", "label.share-url": "共享链接",
"label.single-day": "单日", "label.single-day": "单日",
"label.start-step": "Start Step", "label.start-step": "开始步骤",
"label.steps": "步骤", "label.steps": "步骤",
"label.sum": "总和", "label.sum": "总和",
"label.tablet": "平板", "label.tablet": "平板",
"label.team": "团队", "label.team": "团队",
"label.team-id": "团队 ID", "label.team-id": "团队 ID",
"label.team-manager": "Team manager", "label.team-manager": "团队管理者",
"label.team-member": "团队成员", "label.team-member": "团队成员",
"label.team-name": "团队名称", "label.team-name": "团队名称",
"label.team-owner": "团队所有者", "label.team-owner": "团队所有者",
@ -225,7 +225,7 @@
"label.yesterday": "昨天", "label.yesterday": "昨天",
"message.action-confirmation": "在下面的框中输入 {confirmation} 以确认。", "message.action-confirmation": "在下面的框中输入 {confirmation} 以确认。",
"message.active-users": "当前在线 {x} 人", "message.active-users": "当前在线 {x} 人",
"message.collected-data": "Collected data", "message.collected-data": "已收集的数据",
"message.confirm-delete": "你确定要删除 {target} 吗?", "message.confirm-delete": "你确定要删除 {target} 吗?",
"message.confirm-leave": "你确定要离开 {target} 吗?", "message.confirm-leave": "你确定要离开 {target} 吗?",
"message.confirm-remove": "您确定要移除 {target} ", "message.confirm-remove": "您确定要移除 {target} ",

View File

@ -2,18 +2,19 @@ import { ClickHouseClient, createClient } from '@clickhouse/client';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import debug from 'debug'; import debug from 'debug';
import { CLICKHOUSE } from 'lib/db'; import { CLICKHOUSE } from 'lib/db';
import { QueryFilters, QueryOptions } from './types'; import { PageParams, QueryFilters, QueryOptions } from './types';
import { OPERATORS } from './constants'; import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants';
import { fetchWebsite } from './load'; import { fetchWebsite } from './load';
import { maxDate } from './date'; import { maxDate } from './date';
import { filtersToArray } from './params'; import { filtersToArray } from './params';
export const CLICKHOUSE_DATE_FORMATS = { export const CLICKHOUSE_DATE_FORMATS = {
minute: '%Y-%m-%d %H:%i:00', second: '%Y-%m-%dT%H:%i:%S',
hour: '%Y-%m-%d %H:00:00', minute: '%Y-%m-%dT%H:%i:00',
day: '%Y-%m-%d', hour: '%Y-%m-%dT%H:00:00',
month: '%Y-%m-01', day: '%Y-%m-%dT00:00:00',
year: '%Y-01-01', month: '%Y-%m-01T00:00:00',
year: '%Y-01-01T00:00:00',
}; };
const log = debug('umami:clickhouse'); const log = debug('umami:clickhouse');
@ -32,7 +33,7 @@ function getClient() {
} = new URL(process.env.CLICKHOUSE_URL); } = new URL(process.env.CLICKHOUSE_URL);
const client = createClient({ const client = createClient({
host: `${protocol}//${hostname}:${port}`, url: `${protocol}//${hostname}:${port}`,
database: pathname.replace('/', ''), database: pathname.replace('/', ''),
username: username, username: username,
password, password,
@ -47,11 +48,15 @@ function getClient() {
return client; return client;
} }
function getDateStringQuery(data: any, unit: string | number) { function getDateStringSQL(data: any, unit: string | number, timezone?: string) {
if (timezone) {
return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}', '${timezone}')`;
}
return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}')`; return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}')`;
} }
function getDateQuery(field: string, unit: string, timezone?: string) { function getDateSQL(field: string, unit: string, timezone?: string) {
if (timezone) { if (timezone) {
return `date_trunc('${unit}', ${field}, '${timezone}')`; return `date_trunc('${unit}', ${field}, '${timezone}')`;
} }
@ -95,6 +100,20 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
return query.join('\n'); return query.join('\n');
} }
function getDateQuery(filters: QueryFilters = {}) {
const { startDate, endDate } = filters;
if (startDate) {
if (endDate) {
return `and created_at between {startDate:DateTime64} and {endDate:DateTime64}`;
} else {
return `and created_at >= {startDate:DateTime64}`;
}
}
return '';
}
function getFilterParams(filters: QueryFilters = {}) { function getFilterParams(filters: QueryFilters = {}) {
return filtersToArray(filters).reduce((obj, { name, value }) => { return filtersToArray(filters).reduce((obj, { name, value }) => {
if (name && value !== undefined) { if (name && value !== undefined) {
@ -110,6 +129,7 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio
return { return {
filterQuery: getFilterQuery(filters, options), filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
params: { params: {
...getFilterParams(filters), ...getFilterParams(filters),
websiteId, websiteId,
@ -119,6 +139,32 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio
}; };
} }
async function pagedQuery(
query: string,
queryParams: { [key: string]: any },
pageParams: PageParams = {},
) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams;
const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (page - 1);
const direction = sortDescending ? 'desc' : 'asc';
const statements = [
orderBy && `order by ${orderBy} ${direction}`,
+size > 0 && `limit ${+size} offset ${offset}`,
]
.filter(n => n)
.join('\n');
const count = await rawQuery(`select count(*) as num from (${query}) t`, queryParams).then(
res => res[0].num,
);
const data = await rawQuery(`${query}${statements}`, queryParams);
return { data, count, page: +page, pageSize: size, orderBy };
}
async function rawQuery<T = unknown>( async function rawQuery<T = unknown>(
query: string, query: string,
params: Record<string, unknown> = {}, params: Record<string, unknown> = {},
@ -136,7 +182,13 @@ async function rawQuery<T = unknown>(
format: 'JSONEachRow', format: 'JSONEachRow',
}); });
return resultSet.json(); return resultSet.json() as T;
}
async function insert(table: string, values: any[]) {
await connect();
return clickhouse.insert({ table, values, format: 'JSONEachRow' });
} }
async function findUnique(data: any[]) { async function findUnique(data: any[]) {
@ -164,12 +216,14 @@ export default {
client: clickhouse, client: clickhouse,
log, log,
connect, connect,
getDateStringQuery, getDateStringSQL,
getDateQuery, getDateSQL,
getDateFormat, getDateFormat,
getFilterQuery, getFilterQuery,
parseFilters, parseFilters,
pagedQuery,
findUnique, findUnique,
findFirst, findFirst,
rawQuery, rawQuery,
insert,
}; };

View File

@ -58,8 +58,8 @@ export const SESSION_COLUMNS = [
export const FILTER_COLUMNS = { export const FILTER_COLUMNS = {
url: 'url_path', url: 'url_path',
entry: 'url_path', entry: 'entry_url',
exit: 'url_path', exit: 'exit_url',
referrer: 'referrer_domain', referrer: 'referrer_domain',
host: 'hostname', host: 'hostname',
title: 'page_title', title: 'page_title',
@ -248,7 +248,8 @@ export const UUID_REGEX =
/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/; /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
export const HOSTNAME_REGEX = export const HOSTNAME_REGEX =
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
export const IP_REGEX = /^((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.(?!$)|$)){4}$/; export const IP_REGEX =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:(?:(:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/;
export const DATETIME_REGEX = export const DATETIME_REGEX =
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3}(Z|\+[0-9]{2}:[0-9]{2})?)?$/; /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3}(Z|\+[0-9]{2}:[0-9]{2})?)?$/;

View File

@ -292,7 +292,7 @@ export function getDateArray(data: any[], startDate: Date, endDate: Date, unit:
for (let i = 0; i <= n; i++) { for (let i = 0; i <= n; i++) {
const t = start(add(startDate, i)); const t = start(add(startDate, i));
const y = data.find(({ x }) => start(getDateFromString(x)).getTime() === t.getTime())?.y || 0; const y = data.find(({ x }) => start(new Date(x)).getTime() === t.getTime())?.y || 0;
arr.push({ x: t, y }); arr.push({ x: t, y });
} }

View File

@ -2,9 +2,9 @@ import path from 'path';
import { getClientIp } from 'request-ip'; import { getClientIp } from 'request-ip';
import { browserName, detectOS } from 'detect-browser'; import { browserName, detectOS } from 'detect-browser';
import isLocalhost from 'is-localhost-ip'; import isLocalhost from 'is-localhost-ip';
import ipaddr from 'ipaddr.js';
import maxmind from 'maxmind'; import maxmind from 'maxmind';
import { safeDecodeURIComponent } from 'next-basics'; import { safeDecodeURIComponent } from 'next-basics';
import { import {
DESKTOP_OS, DESKTOP_OS,
MOBILE_OS, MOBILE_OS,
@ -137,3 +137,31 @@ export async function getClientInfo(req: NextApiRequestCollect) {
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device }; return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
} }
export function hasBlockedIp(req: NextApiRequestCollect) {
const ignoreIps = process.env.IGNORE_IP;
if (ignoreIps) {
const ips = [];
if (ignoreIps) {
ips.push(...ignoreIps.split(',').map(n => n.trim()));
}
const clientIp = getIpAddress(req);
return ips.find(ip => {
if (ip === clientIp) return true;
// CIDR notation
if (ip.indexOf('/') > 0) {
const addr = ipaddr.parse(clientIp);
const range = ipaddr.parseCIDR(ip);
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
}
});
}
return false;
}

View File

@ -61,8 +61,8 @@ function getDateFormat(date: Date, format?: string): string {
} }
async function sendMessage( async function sendMessage(
message: { [key: string]: string | number },
topic: string, topic: string,
message: { [key: string]: string | number },
): Promise<RecordMetadata[]> { ): Promise<RecordMetadata[]> {
await connect(); await connect();
@ -77,7 +77,7 @@ async function sendMessage(
}); });
} }
async function sendMessages(messages: { [key: string]: string | number }[], topic: string) { async function sendMessages(topic: string, messages: { [key: string]: string | number }[]) {
await connect(); await connect();
await producer.send({ await producer.send({

View File

@ -60,7 +60,7 @@ function getCastColumnQuery(field: string, type: string): string {
} }
} }
function getDateQuery(field: string, unit: string, timezone?: string): string { function getDateSQL(field: string, unit: string, timezone?: string): string {
const db = getDatabaseType(); const db = getDatabaseType();
if (db === POSTGRESQL) { if (db === POSTGRESQL) {
@ -81,7 +81,19 @@ function getDateQuery(field: string, unit: string, timezone?: string): string {
} }
} }
function getTimestampDiffQuery(field1: string, field2: string): string { export function getTimestampSQL(field: string) {
const db = getDatabaseType();
if (db === POSTGRESQL) {
return `floor(extract(epoch from ${field}))`;
}
if (db === MYSQL) {
return `UNIX_TIMESTAMP(${field})`;
}
}
function getTimestampDiffSQL(field1: string, field2: string): string {
const db = getDatabaseType(); const db = getDatabaseType();
if (db === POSTGRESQL) { if (db === POSTGRESQL) {
@ -93,7 +105,7 @@ function getTimestampDiffQuery(field1: string, field2: string): string {
} }
} }
function getSearchQuery(column: string): string { function getSearchSQL(column: string): string {
const db = getDatabaseType(); const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like'; const like = db === POSTGRESQL ? 'ilike' : 'like';
@ -137,6 +149,20 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}):
return query.join('\n'); return query.join('\n');
} }
function getDateQuery(filters: QueryFilters = {}) {
const { startDate, endDate } = filters;
if (startDate) {
if (endDate) {
return `and website_event.created_at between {{startDate}} and {{endDate}}`;
} else {
return `and website_event.created_at >= {{startDate}}`;
}
}
return '';
}
function getFilterParams(filters: QueryFilters = {}) { function getFilterParams(filters: QueryFilters = {}) {
return filtersToArray(filters).reduce((obj, { name, operator, value }) => { return filtersToArray(filters).reduce((obj, { name, operator, value }) => {
obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator) obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator)
@ -161,6 +187,7 @@ async function parseFilters(
? `inner join session on website_event.session_id = session.session_id` ? `inner join session on website_event.session_id = session.session_id`
: '', : '',
filterQuery: getFilterQuery(filters, options), filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
params: { params: {
...getFilterParams(filters), ...getFilterParams(filters),
websiteId, websiteId,
@ -191,8 +218,8 @@ async function rawQuery(sql: string, data: object): Promise<any> {
return prisma.rawQuery(query, params); return prisma.rawQuery(query, params);
} }
async function pagedQuery<T>(model: string, criteria: T, filters: PageParams) { async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams) {
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {}; const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {};
const size = +pageSize || DEFAULT_PAGE_SIZE; const size = +pageSize || DEFAULT_PAGE_SIZE;
const data = await prisma.client[model].findMany({ const data = await prisma.client[model].findMany({
@ -256,11 +283,11 @@ export default {
getAddIntervalQuery, getAddIntervalQuery,
getCastColumnQuery, getCastColumnQuery,
getDayDiffQuery, getDayDiffQuery,
getDateQuery, getDateSQL,
getFilterQuery, getFilterQuery,
getSearchParameters, getSearchParameters,
getTimestampDiffQuery, getTimestampDiffSQL,
getSearchQuery, getSearchSQL,
getQueryMode, getQueryMode,
pagedQuery, pagedQuery,
parseFilters, parseFilters,

View File

@ -1,24 +0,0 @@
import { ok } from 'next-basics';
import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants';
import { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (process.env.NODE_ENV === 'production') {
res.setHeader('content-type', 'text/javascript');
if (process.env.DISABLE_TELEMETRY || process.env.PRIVATE_MODE) {
return res.send('/* telemetry disabled */');
}
const script = `
(()=>{const i=document.createElement('img');
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
document.body.appendChild(i);})();
`;
return res.send(script.replace(/\s\s+/g, ''));
}
return ok(res);
}

View File

@ -1,4 +1,3 @@
import ipaddr from 'ipaddr.js';
import { isbot } from 'isbot'; import { isbot } from 'isbot';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { import {
@ -12,7 +11,7 @@ import {
} from 'next-basics'; } from 'next-basics';
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants'; import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
import { secret, visitSalt, uuid } from 'lib/crypto'; import { secret, visitSalt, uuid } from 'lib/crypto';
import { getIpAddress } from 'lib/detect'; import { hasBlockedIp } from 'lib/detect';
import { useCors, useSession, useValidate } from 'lib/middleware'; import { useCors, useSession, useValidate } from 'lib/middleware';
import { CollectionType, YupRequest } from 'lib/types'; import { CollectionType, YupRequest } from 'lib/types';
import { saveEvent, saveSessionData } from 'queries'; import { saveEvent, saveSessionData } from 'queries';
@ -122,7 +121,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
urlPath = '/'; urlPath = '/';
} }
if (referrerPath?.startsWith('http')) { if (/^[\w-]+:\/\/\w+/.test(referrerPath)) {
const refUrl = new URL(referrer); const refUrl = new URL(referrer);
referrerPath = refUrl.pathname; referrerPath = refUrl.pathname;
referrerQuery = refUrl.search.substring(1); referrerQuery = refUrl.search.substring(1);
@ -166,31 +165,3 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
return methodNotAllowed(res); return methodNotAllowed(res);
}; };
function hasBlockedIp(req: NextApiRequestCollect) {
const ignoreIps = process.env.IGNORE_IP;
if (ignoreIps) {
const ips = [];
if (ignoreIps) {
ips.push(...ignoreIps.split(',').map(n => n.trim()));
}
const clientIp = getIpAddress(req);
return ips.find(ip => {
if (ip === clientIp) return true;
// CIDR notation
if (ip.indexOf('/') > 0) {
const addr = ipaddr.parse(clientIp);
const range = ipaddr.parseCIDR(ip);
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
}
});
}
return false;
}

View File

@ -64,7 +64,7 @@ export default async (
await useAuth(req, res); await useAuth(req, res);
await useValidate(schema, req, res); await useValidate(schema, req, res);
const { websiteId, type, limit, offset, search } = req.query; const { websiteId, type, limit, offset, search, unit } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) { if (!(await canViewWebsite(req.auth, websiteId))) {
@ -89,7 +89,7 @@ export default async (
} }
if (SESSION_COLUMNS.includes(type)) { if (SESSION_COLUMNS.includes(type)) {
const data = await getSessionMetrics(websiteId, type, filters, limit, offset); const data = await getSessionMetrics(websiteId, type, filters, limit, offset, unit as string);
if (type === 'language') { if (type === 'language') {
const combined = {}; const combined = {};
@ -111,7 +111,14 @@ export default async (
} }
if (EVENT_COLUMNS.includes(type)) { if (EVENT_COLUMNS.includes(type)) {
const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); const data = await getPageviewMetrics(
websiteId,
type,
filters,
limit,
offset,
unit as string,
);
return ok(res, data); return ok(res, data);
} }

View File

@ -0,0 +1,42 @@
import * as yup from 'yup';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { pageInfo } from 'lib/schema';
import { getSessions } from 'queries';
export interface ReportsRequestQuery extends PageParams {
websiteId: string;
}
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
...pageInfo,
}),
};
export default async (
req: NextApiRequestQueryBody<ReportsRequestQuery, any>,
res: NextApiResponse,
) => {
await useCors(req, res);
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
return unauthorized(res);
}
const data = await getSessions(websiteId, {}, req.query);
return ok(res, data);
}
return methodNotAllowed(res);
};

View File

@ -56,7 +56,7 @@ export default async (
await useAuth(req, res); await useAuth(req, res);
await useValidate(schema, req, res); await useValidate(schema, req, res);
const { websiteId, compare } = req.query; const { websiteId, compare, unit } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) { if (!(await canViewWebsite(req.auth, websiteId))) {
@ -72,9 +72,13 @@ export default async (
const filters = getRequestFilters(req); const filters = getRequestFilters(req);
const metrics = await getWebsiteStats(websiteId, { ...filters, startDate, endDate }); const metrics = await getWebsiteStats(websiteId, unit as string, {
...filters,
startDate,
endDate,
});
const prevPeriod = await getWebsiteStats(websiteId, { const prevPeriod = await getWebsiteStats(websiteId, unit as string, {
...filters, ...filters,
startDate: compareStartDate, startDate: compareStartDate,
endDate: compareEndDate, endDate: compareEndDate,

View File

@ -3,6 +3,7 @@ import { DATA_TYPE } from 'lib/constants';
import { uuid } from 'lib/crypto'; import { uuid } from 'lib/crypto';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import { flattenJSON, getStringValue } from 'lib/data'; import { flattenJSON, getStringValue } from 'lib/data';
import clickhouse from 'lib/clickhouse';
import kafka from 'lib/kafka'; import kafka from 'lib/kafka';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { DynamicData } from 'lib/types'; import { DynamicData } from 'lib/types';
@ -59,6 +60,7 @@ async function clickhouseQuery(data: {
}) { }) {
const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data; const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data;
const { insert } = clickhouse;
const { getDateFormat, sendMessages } = kafka; const { getDateFormat, sendMessages } = kafka;
const jsonKeys = flattenJSON(eventData); const jsonKeys = flattenJSON(eventData);
@ -79,7 +81,11 @@ async function clickhouseQuery(data: {
}; };
}); });
await sendMessages(messages, 'event_data'); if (kafka.enabled) {
await sendMessages('event_data', messages);
} else {
await insert('event_data', messages);
}
return data; return data;
} }

View File

@ -15,7 +15,7 @@ export async function getEventMetrics(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateQuery, parseFilters } = prisma; const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, { const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
@ -25,7 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
` `
select select
event_name x, event_name x,
${getDateQuery('website_event.created_at', unit, timezone)} t, ${getDateSQL('website_event.created_at', unit, timezone)} t,
count(*) y count(*) y
from website_event from website_event
${joinSession} ${joinSession}
@ -45,23 +45,29 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; t: string; y: number }[]> { ): Promise<{ x: string; t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters; const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateQuery, parseFilters } = clickhouse; const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, { const { filterQuery, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
}); });
const table = unit === 'hour' ? 'website_event_stats_hourly' : 'website_event_stats_daily';
return rawQuery( return rawQuery(
` `
select select
event_name x, event_name x,
${getDateQuery('created_at', unit, timezone)} t, ${getDateSQL('created_at', unit, timezone)} t,
count(*) y count(*) y
from website_event from (
where website_id = {websiteId:UUID} select arrayJoin(event_name) as event_name,
and created_at between {startDate:DateTime64} and {endDate:DateTime64} created_at
and event_type = {eventType:UInt32} from ${table} website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery} ${filterQuery}
) as g
group by x, t group by x, t
order by t order by t
`, `,

View File

@ -1,63 +1,54 @@
import clickhouse from 'lib/clickhouse'; import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { QueryFilters } from 'lib/types'; import { PageParams, QueryFilters } from 'lib/types';
export function getEvents(...args: [websiteId: string, filters: QueryFilters]) { export function getEvents(
...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
}); });
} }
function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { startDate } = filters; const { pagedQuery } = prisma;
return prisma.client.websiteEvent const where = {
.findMany({ ...filters,
where: { id: websiteId,
websiteId, };
createdAt: {
gte: startDate, return pagedQuery('website_event', { where }, pageParams);
},
},
orderBy: {
createdAt: 'desc',
},
})
.then(a => {
return Object.values(a).map(a => {
return {
...a,
timestamp: new Date(a.createdAt).getTime() / 1000,
};
});
});
} }
function clickhouseQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { rawQuery } = clickhouse; const { pagedQuery, parseFilters, getDateStringSQL } = clickhouse;
const { startDate } = filters; const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
return rawQuery( return pagedQuery(
` `
select select
event_id as id, event_id as id,
website_id as websiteId, website_id as websiteId,
session_id as sessionId, session_id as sessionId,
created_at as createdAt, ${getDateStringSQL('created_at', 'second', filters.timezone)} as createdAt,
toUnixTimestamp(created_at) as timestamp,
url_path as urlPath, url_path as urlPath,
url_query as urlQuery,
referrer_path as referrerPath,
referrer_query as referrerQuery,
referrer_domain as referrerDomain, referrer_domain as referrerDomain,
page_title as pageTitle,
event_type as eventType,
event_name as eventName event_name as eventName
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64} ${dateQuery}
${filterQuery}
order by created_at desc order by created_at desc
`, `,
{ params,
websiteId, pageParams,
startDate,
},
); );
} }

View File

@ -1,5 +1,6 @@
import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE, PAGE_TITLE_LENGTH } from 'lib/constants'; import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE, PAGE_TITLE_LENGTH } from 'lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import clickhouse from 'lib/clickhouse';
import kafka from 'lib/kafka'; import kafka from 'lib/kafka';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { uuid } from 'lib/crypto'; import { uuid } from 'lib/crypto';
@ -134,6 +135,7 @@ async function clickhouseQuery(data: {
city, city,
...args ...args
} = data; } = data;
const { insert } = clickhouse;
const { getDateFormat, sendMessage } = kafka; const { getDateFormat, sendMessage } = kafka;
const eventId = uuid(); const eventId = uuid();
const createdAt = getDateFormat(new Date()); const createdAt = getDateFormat(new Date());
@ -164,7 +166,11 @@ async function clickhouseQuery(data: {
created_at: createdAt, created_at: createdAt,
}; };
await sendMessage(message, 'event'); if (kafka.enabled) {
await sendMessage('event', message);
} else {
await insert('website_event', [message]);
}
if (eventData) { if (eventData) {
await saveEventData({ await saveEventData({

View File

@ -19,15 +19,15 @@ export async function getRealtimeData(
const { startDate, timezone } = criteria; const { startDate, timezone } = criteria;
const filters = { startDate, endDate: new Date(), unit: 'minute', timezone }; const filters = { startDate, endDate: new Date(), unit: 'minute', timezone };
const [events, sessions, pageviews, sessionviews] = await Promise.all([ const [events, sessions, pageviews, sessionviews] = await Promise.all([
getEvents(websiteId, { startDate }), getEvents(websiteId, { startDate, timezone }, { pageSize: 10000 }),
getSessions(websiteId, { startDate }), getSessions(websiteId, { startDate, timezone }, { pageSize: 10000 }),
getPageviewStats(websiteId, filters), getPageviewStats(websiteId, filters),
getSessionStats(websiteId, filters), getSessionStats(websiteId, filters),
]); ]);
const uniques = new Set(); const uniques = new Set();
const sessionStats = sessions.reduce( const sessionStats = sessions.data.reduce(
(obj: { visitors: any; countries: any }, session: { id: any; country: any }) => { (obj: { visitors: any; countries: any }, session: { id: any; country: any }) => {
const { countries, visitors } = obj; const { countries, visitors } = obj;
const { id, country } = session; const { id, country } = session;
@ -49,7 +49,7 @@ export async function getRealtimeData(
}, },
); );
const eventStats = events.reduce( const eventStats = events.data.reduce(
( (
obj: { urls: any; referrers: any; events: any }, obj: { urls: any; referrers: any; events: any },
event: { urlPath: any; referrerDomain: any }, event: { urlPath: any; referrerDomain: any },
@ -81,9 +81,9 @@ export async function getRealtimeData(
visitors: sessionviews, visitors: sessionviews,
}, },
totals: { totals: {
views: events.filter(e => !e.eventName).length, views: events.data.filter(e => !e.eventName).length,
visitors: uniques.size, visitors: uniques.size,
events: events.filter(e => e.eventName).length, events: events.data.filter(e => e.eventName).length,
countries: Object.keys(sessionStats.countries).length, countries: Object.keys(sessionStats.countries).length,
}, },
timestamp: Date.now(), timestamp: Date.now(),

View File

@ -18,11 +18,11 @@ async function relationalQuery(
endDate: Date, endDate: Date,
search: string, search: string,
) { ) {
const { rawQuery, getSearchQuery } = prisma; const { rawQuery, getSearchSQL } = prisma;
let searchQuery = ''; let searchQuery = '';
if (search) { if (search) {
searchQuery = getSearchQuery(column); searchQuery = getSearchSQL(column);
} }
return rawQuery( return rawQuery(

View File

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
import clickhouse from 'lib/clickhouse'; import clickhouse from 'lib/clickhouse';
import { EVENT_TYPE } from 'lib/constants'; import { EVENT_TYPE } from 'lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
@ -5,7 +6,7 @@ import prisma from 'lib/prisma';
import { QueryFilters } from 'lib/types'; import { QueryFilters } from 'lib/types';
export async function getWebsiteStats( export async function getWebsiteStats(
...args: [websiteId: string, filters: QueryFilters] ...args: [websiteId: string, unit: string, filters: QueryFilters]
): Promise< ): Promise<
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[] { pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> { > {
@ -17,11 +18,12 @@ export async function getWebsiteStats(
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
unit: string,
filters: QueryFilters, filters: QueryFilters,
): Promise< ): Promise<
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[] { pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> { > {
const { getTimestampDiffQuery, parseFilters, rawQuery } = prisma; const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, { const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -34,7 +36,7 @@ async function relationalQuery(
count(distinct t.session_id) as "visitors", count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits", count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces", sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffQuery('t.min_time', 't.max_time')}) as "totaltime" sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from ( from (
select select
website_event.session_id, website_event.session_id,
@ -57,6 +59,7 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
unit: string,
filters: QueryFilters, filters: QueryFilters,
): Promise< ): Promise<
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[] { pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
@ -66,29 +69,21 @@ async function clickhouseQuery(
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
const table = unit === 'hour' ? 'website_event_stats_hourly' : 'website_event_stats_daily';
return rawQuery( return rawQuery(
` `
select select
sum(t.c) as "pageviews", sum(views) as "pageviews",
count(distinct t.session_id) as "visitors", uniq(session_id) as "visitors",
count(distinct t.visit_id) as "visits", uniq(visit_id) as "visits",
sum(if(t.c = 1, 1, 0)) as "bounces", sumIf(1, views = 1) as "bounces",
sum(max_time-min_time) as "totaltime" sum(max_time-min_time) as "totaltime"
from ( from ${table} "website_event"
select where website_id = {websiteId:UUID}
session_id, and created_at between {startDate:DateTime64} and {endDate:DateTime64}
visit_id, and event_type = {eventType:UInt32}
count(*) c, ${filterQuery};
min(created_at) min_time,
max(created_at) max_time
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery}
group by session_id, visit_id
) as t;
`, `,
params, params,
).then(result => { ).then(result => {

View File

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
import clickhouse from 'lib/clickhouse'; import clickhouse from 'lib/clickhouse';
import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
@ -5,7 +6,14 @@ import prisma from 'lib/prisma';
import { QueryFilters } from 'lib/types'; import { QueryFilters } from 'lib/types';
export async function getPageviewMetrics( export async function getPageviewMetrics(
...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number] ...args: [
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number,
offset?: number,
unit?: string,
]
) { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -19,6 +27,7 @@ async function relationalQuery(
filters: QueryFilters, filters: QueryFilters,
limit: number = 500, limit: number = 500,
offset: number = 0, offset: number = 0,
unit: string,
) { ) {
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
@ -42,15 +51,18 @@ async function relationalQuery(
const aggregrate = type === 'entry' ? 'min' : 'max'; const aggregrate = type === 'entry' ? 'min' : 'max';
entryExitQuery = ` entryExitQuery = `
JOIN (select visit_id, join (
${aggregrate}(created_at) target_created_at select visit_id,
from website_event ${aggregrate}(created_at) target_created_at
where website_event.website_id = {{websiteId::uuid}} from website_event
and website_event.created_at between {{startDate}} and {{endDate}} where website_event.website_id = {{websiteId::uuid}}
and event_type = {{eventType}} and website_event.created_at between {{startDate}} and {{endDate}}
group by visit_id) x and event_type = {{eventType}}
ON x.visit_id = website_event.visit_id group by visit_id
and x.target_created_at = website_event.created_at`; ) x
on x.visit_id = website_event.visit_id
and x.target_created_at = website_event.created_at
`;
} }
return rawQuery( return rawQuery(
@ -79,6 +91,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
limit: number = 500, limit: number = 500,
offset: number = 0, offset: number = 0,
unit: string,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
@ -87,37 +100,42 @@ async function clickhouseQuery(
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
}); });
let entryExitQuery = '';
let excludeDomain = ''; let excludeDomain = '';
let groupByQuery = '';
if (column === 'referrer_domain') { if (column === 'referrer_domain') {
excludeDomain = `and referrer_domain != {websiteDomain:String} and referrer_domain != ''`; excludeDomain = `and t != {websiteDomain:String} and t != ''`;
}
let columnQuery = `arrayJoin(${column})`;
if (type === 'entry') {
columnQuery = `visit_id x, argMinMerge(${column})`;
}
if (type === 'exit') {
columnQuery = `visit_id x, argMaxMerge(${column})`;
} }
if (type === 'entry' || type === 'exit') { if (type === 'entry' || type === 'exit') {
const aggregrate = type === 'entry' ? 'min' : 'max'; groupByQuery = 'group by x';
entryExitQuery = `
JOIN (select visit_id,
${aggregrate}(created_at) target_created_at
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
group by visit_id) x
ON x.visit_id = website_event.visit_id
and x.target_created_at = website_event.created_at`;
} }
const table = unit === 'hour' ? 'website_event_stats_hourly' : 'website_event_stats_daily';
return rawQuery( return rawQuery(
` `
select ${column} x, count(*) y select g.t as x,
from website_event count(*) as y
${entryExitQuery} from (
where website_id = {websiteId:UUID} select ${columnQuery} as t
and created_at between {startDate:DateTime64} and {endDate:DateTime64} from ${table} website_event
and event_type = {eventType:UInt32} where website_id = {websiteId:UUID}
${excludeDomain} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery} and event_type = {eventType:UInt32}
${excludeDomain}
${filterQuery}
${groupByQuery}) as g
group by x group by x
order by y desc order by y desc
limit ${limit} limit ${limit}

View File

@ -13,7 +13,7 @@ export async function getPageviewStats(...args: [websiteId: string, filters: Que
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { getDateQuery, parseFilters, rawQuery } = prisma; const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, { const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -22,7 +22,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
return rawQuery( return rawQuery(
` `
select select
${getDateQuery('website_event.created_at', unit, timezone)} x, ${getDateSQL('website_event.created_at', unit, timezone)} x,
count(*) y count(*) y
from website_event from website_event
${joinSession} ${joinSession}
@ -41,22 +41,23 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters; const { timezone = 'UTC', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; const { parseFilters, rawQuery, getDateStringSQL, getDateSQL } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, { const { filterQuery, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
const table = unit === 'hour' ? 'website_event_stats_hourly' : 'website_event_stats_daily';
return rawQuery( return rawQuery(
` `
select select
${getDateStringQuery('g.t', unit)} as x, ${getDateStringSQL('g.t', unit)} as x,
g.y as y g.y as y
from ( from (
select select
${getDateQuery('created_at', unit, timezone)} as t, ${getDateSQL('created_at', unit, timezone)} as t,
count(*) as y sum(views) as y
from website_event from ${table} website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}

View File

@ -23,7 +23,7 @@ async function relationalQuery(
y: number; y: number;
}[] }[]
> { > {
const { getTimestampDiffQuery, parseFilters, rawQuery } = prisma; const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters( const { filterQuery, joinSession, params } = await parseFilters(
websiteId, websiteId,
{ {
@ -42,7 +42,7 @@ async function relationalQuery(
count(distinct t.session_id) as "visitors", count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits", count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces", sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffQuery('t.min_time', 't.max_time')}) as "totaltime", sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime",
${parseFieldsByName(fields)} ${parseFieldsByName(fields)}
from ( from (
select select

View File

@ -35,14 +35,14 @@ async function relationalQuery(
}[] }[]
> { > {
const { startDate, endDate, timezone = 'UTC' } = filters; const { startDate, endDate, timezone = 'UTC' } = filters;
const { getDateQuery, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma; const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
const unit = 'day'; const unit = 'day';
return rawQuery( return rawQuery(
` `
WITH cohort_items AS ( WITH cohort_items AS (
select session_id, select session_id,
${getDateQuery('created_at', unit, timezone)} as cohort_date ${getDateSQL('created_at', unit, timezone)} as cohort_date
from session from session
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
@ -50,10 +50,7 @@ async function relationalQuery(
user_activities AS ( user_activities AS (
select distinct select distinct
w.session_id, w.session_id,
${getDayDiffQuery( ${getDayDiffQuery(getDateSQL('created_at', unit, timezone), 'c.cohort_date')} as day_number
getDateQuery('created_at', unit, timezone),
'c.cohort_date',
)} as day_number
from website_event w from website_event w
join cohort_items c join cohort_items c
on w.session_id = c.session_id on w.session_id = c.session_id
@ -115,14 +112,14 @@ async function clickhouseQuery(
}[] }[]
> { > {
const { startDate, endDate, timezone = 'UTC' } = filters; const { startDate, endDate, timezone = 'UTC' } = filters;
const { getDateQuery, getDateStringQuery, rawQuery } = clickhouse; const { getDateSQL, getDateStringSQL, rawQuery } = clickhouse;
const unit = 'day'; const unit = 'day';
return rawQuery( return rawQuery(
` `
WITH cohort_items AS ( WITH cohort_items AS (
select select
min(${getDateQuery('created_at', unit, timezone)}) as cohort_date, min(${getDateSQL('created_at', unit, timezone)}) as cohort_date,
session_id session_id
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
@ -132,7 +129,7 @@ async function clickhouseQuery(
user_activities AS ( user_activities AS (
select distinct select distinct
w.session_id, w.session_id,
(${getDateQuery('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number (${getDateSQL('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number
from website_event w from website_event w
join cohort_items c join cohort_items c
on w.session_id = c.session_id on w.session_id = c.session_id
@ -157,7 +154,7 @@ async function clickhouseQuery(
group by 1, 2 group by 1, 2
) )
select select
${getDateStringQuery('c.cohort_date', unit)} as date, ${getDateStringSQL('c.cohort_date', unit)} as date,
c.day_number as day, c.day_number as day,
s.visitors as visitors, s.visitors as visitors,
c.visitors returnVisitors, c.visitors returnVisitors,

View File

@ -46,12 +46,12 @@ async function relationalQuery(
timezone = 'UTC', timezone = 'UTC',
unit = 'day', unit = 'day',
} = criteria; } = criteria;
const { getDateQuery, rawQuery } = prisma; const { getDateSQL, rawQuery } = prisma;
const chartRes = await rawQuery( const chartRes = await rawQuery(
` `
select select
${getDateQuery('website_event.created_at', unit, timezone)} time, ${getDateSQL('website_event.created_at', unit, timezone)} time,
sum(case when data_key = {{revenueProperty}} then number_value else 0 end) sum, sum(case when data_key = {{revenueProperty}} then number_value else 0 end) sum,
avg(case when data_key = {{revenueProperty}} then number_value else 0 end) avg, avg(case when data_key = {{revenueProperty}} then number_value else 0 end) avg,
count(case when data_key = {{revenueProperty}} then 1 else 0 end) count, count(case when data_key = {{revenueProperty}} then 1 else 0 end) count,
@ -110,7 +110,7 @@ async function clickhouseQuery(
timezone = 'UTC', timezone = 'UTC',
unit = 'day', unit = 'day',
} = criteria; } = criteria;
const { getDateStringQuery, getDateQuery, rawQuery } = clickhouse; const { getDateStringSQL, getDateSQL, rawQuery } = clickhouse;
const chartRes = await rawQuery<{ const chartRes = await rawQuery<{
time: string; time: string;
@ -121,14 +121,14 @@ async function clickhouseQuery(
}>( }>(
` `
select select
${getDateStringQuery('g.time', unit)} as time, ${getDateStringSQL('g.time', unit)} as time,
g.sum as sum, g.sum as sum,
g.avg as avg, g.avg as avg,
g.count as count, g.count as count,
g.uniqueCount as uniqueCount g.uniqueCount as uniqueCount
from ( from (
select select
${getDateQuery('created_at', unit, timezone)} as time, ${getDateSQL('created_at', unit, timezone)} as time,
sumIf(number_value, data_key = {revenueProperty:String}) as sum, sumIf(number_value, data_key = {revenueProperty:String}) as sum,
avgIf(number_value, data_key = {revenueProperty:String}) as avg, avgIf(number_value, data_key = {revenueProperty:String}) as avg,
countIf(data_key = {revenueProperty:String}) as count, countIf(data_key = {revenueProperty:String}) as count,

View File

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse'; import clickhouse from 'lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
@ -5,7 +6,14 @@ import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants';
import { QueryFilters } from 'lib/types'; import { QueryFilters } from 'lib/types';
export async function getSessionMetrics( export async function getSessionMetrics(
...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number] ...args: [
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number,
offset?: number,
unit?: string,
]
) { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -19,6 +27,7 @@ async function relationalQuery(
filters: QueryFilters, filters: QueryFilters,
limit: number = 500, limit: number = 500,
offset: number = 0, offset: number = 0,
unit: string,
) { ) {
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = prisma; const { parseFilters, rawQuery } = prisma;
@ -62,6 +71,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
limit: number = 500, limit: number = 500,
offset: number = 0, offset: number = 0,
unit: string,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = clickhouse; const { parseFilters, rawQuery } = clickhouse;
@ -70,14 +80,15 @@ async function clickhouseQuery(
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
const includeCountry = column === 'city' || column === 'subdivision1'; const includeCountry = column === 'city' || column === 'subdivision1';
const table = unit === 'hour' ? 'website_event_stats_hourly' : 'website_event_stats_daily';
return rawQuery( return rawQuery(
` `
select select
${column} x, ${column} x,
count(distinct session_id) y uniq(session_id) y
${includeCountry ? ', country' : ''} ${includeCountry ? ', country' : ''}
from website_event from ${table} website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}

View File

@ -13,7 +13,7 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { getDateQuery, parseFilters, rawQuery } = prisma; const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, { const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -22,7 +22,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
return rawQuery( return rawQuery(
` `
select select
${getDateQuery('website_event.created_at', unit, timezone)} x, ${getDateSQL('website_event.created_at', unit, timezone)} x,
count(distinct website_event.session_id) y count(distinct website_event.session_id) y
from website_event from website_event
${joinSession} ${joinSession}
@ -41,22 +41,23 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters; const { timezone = 'UTC', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; const { parseFilters, rawQuery, getDateStringSQL, getDateSQL } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, { const { filterQuery, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
const table = unit === 'hour' ? 'website_event_stats_hourly' : 'website_event_stats_daily';
return rawQuery( return rawQuery(
` `
select select
${getDateStringQuery('g.t', unit)} as x, ${getDateStringSQL('g.t', unit)} as x,
g.y as y g.y as y
from ( from (
select select
${getDateQuery('created_at', unit, timezone)} as t, ${getDateSQL('created_at', unit, timezone)} as t,
count(distinct session_id) as y uniq(session_id) as y
from website_event from ${table} website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}

View File

@ -1,51 +1,38 @@
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse'; import clickhouse from 'lib/clickhouse';
import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db';
import { QueryFilters } from 'lib/types'; import { PageParams, QueryFilters } from 'lib/types';
export async function getSessions(...args: [websiteId: string, filters: QueryFilters]) { export async function getSessions(
...args: [websiteId: string, filters?: QueryFilters, pageParams?: PageParams]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
}); });
} }
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams: PageParams) {
const { startDate } = filters; const { pagedQuery } = prisma;
return prisma.client.session const where = {
.findMany({ ...filters,
where: { id: websiteId,
websiteId, };
createdAt: {
gte: startDate, return pagedQuery('session', { where }, pageParams);
},
},
orderBy: {
createdAt: 'desc',
},
})
.then(a => {
return Object.values(a).map(a => {
return {
...a,
timestamp: new Date(a.createdAt).getTime() / 1000,
};
});
});
} }
async function clickhouseQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { rawQuery } = clickhouse; const { pagedQuery, parseFilters, getDateStringSQL } = clickhouse;
const { startDate } = filters; const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
return rawQuery( return pagedQuery(
` `
select select
session_id as id, session_id as id,
website_id as websiteId, website_id as websiteId,
created_at as createdAt, ${getDateStringSQL('created_at', 'second', filters.timezone)} as createdAt,
toUnixTimestamp(created_at) as timestamp,
hostname, hostname,
browser, browser,
os, os,
@ -58,12 +45,11 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
city city
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64} ${dateQuery}
${filterQuery}
order by created_at desc order by created_at desc
`, `,
{ params,
websiteId, pageParams,
startDate,
},
); );
} }

View File

@ -5,6 +5,7 @@ import prisma from 'lib/prisma';
import { DynamicData } from 'lib/types'; import { DynamicData } from 'lib/types';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import kafka from 'lib/kafka'; import kafka from 'lib/kafka';
import clickhouse from 'lib/clickhouse';
export async function saveSessionData(data: { export async function saveSessionData(data: {
websiteId: string; websiteId: string;
@ -81,6 +82,7 @@ async function clickhouseQuery(data: {
}) { }) {
const { websiteId, sessionId, sessionData, createdAt } = data; const { websiteId, sessionId, sessionData, createdAt } = data;
const { insert } = clickhouse;
const { getDateFormat, sendMessages } = kafka; const { getDateFormat, sendMessages } = kafka;
const jsonKeys = flattenJSON(sessionData); const jsonKeys = flattenJSON(sessionData);
@ -98,7 +100,11 @@ async function clickhouseQuery(data: {
}; };
}); });
await sendMessages(messages, 'session_data'); if (kafka.enabled) {
await sendMessages('session_data', messages);
} else {
await insert('session_data', messages);
}
return data; return data;
} }

View File

@ -1,8 +1,8 @@
export * from './admin/report'; export * from 'queries/prisma/report';
export * from './admin/team'; export * from 'queries/prisma/team';
export * from './admin/teamUser'; export * from 'queries/prisma/teamUser';
export * from './admin/user'; export * from 'queries/prisma/user';
export * from './admin/website'; export * from 'queries/prisma/website';
export * from './analytics/events/getEventMetrics'; export * from './analytics/events/getEventMetrics';
export * from './analytics/events/getEventUsage'; export * from './analytics/events/getEventUsage';
export * from './analytics/events/getEvents'; export * from './analytics/events/getEvents';

View File

@ -17,9 +17,9 @@ export async function getReport(reportId: string): Promise<Report> {
export async function getReports( export async function getReports(
criteria: ReportFindManyArgs, criteria: ReportFindManyArgs,
filters: PageParams = {}, pageParams: PageParams = {},
): Promise<PageResult<Report[]>> { ): Promise<PageResult<Report[]>> {
const { query } = filters; const { query } = pageParams;
const where: Prisma.ReportWhereInput = { const where: Prisma.ReportWhereInput = {
...criteria.where, ...criteria.where,
@ -45,7 +45,7 @@ export async function getReports(
]), ]),
}; };
return prisma.pagedQuery('report', { ...criteria, where }, filters); return prisma.pagedQuery('report', { ...criteria, where }, pageParams);
} }
export async function getUserReports( export async function getUserReports(

View File

@ -49,9 +49,9 @@ export async function getUserByUsername(username: string, options: GetUserOption
export async function getUsers( export async function getUsers(
criteria: UserFindManyArgs, criteria: UserFindManyArgs,
filters?: PageParams, pageParams?: PageParams,
): Promise<PageResult<User[]>> { ): Promise<PageResult<User[]>> {
const { query } = filters; const { query } = pageParams;
const where: Prisma.UserWhereInput = { const where: Prisma.UserWhereInput = {
...criteria.where, ...criteria.where,
@ -68,7 +68,7 @@ export async function getUsers(
{ {
orderBy: 'createdAt', orderBy: 'createdAt',
sortDescending: true, sortDescending: true,
...filters, ...pageParams,
}, },
); );
} }

View File

@ -27,9 +27,9 @@ export async function getSharedWebsite(shareId: string) {
export async function getWebsites( export async function getWebsites(
criteria: WebsiteFindManyArgs, criteria: WebsiteFindManyArgs,
filters: PageParams, pageParams: PageParams,
): Promise<PageResult<Website[]>> { ): Promise<PageResult<Website[]>> {
const { query } = filters; const { query } = pageParams;
const where: Prisma.WebsiteWhereInput = { const where: Prisma.WebsiteWhereInput = {
...criteria.where, ...criteria.where,
@ -42,7 +42,7 @@ export async function getWebsites(
deletedAt: null, deletedAt: null,
}; };
return prisma.pagedQuery('website', { ...criteria, where }, filters); return prisma.pagedQuery('website', { ...criteria, where }, pageParams);
} }
export async function getAllWebsites(userId: string) { export async function getAllWebsites(userId: string) {

775
yarn.lock

File diff suppressed because it is too large Load Diff