mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Merge branch 'dev' into feature/table-view-events
This commit is contained in:
commit
31f9b17942
60 changed files with 2522 additions and 1972 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import { uuid } from '../../src/lib/crypto';
|
||||
|
||||
describe('Website API tests', () => {
|
||||
Cypress.session.clearAllSavedSessions();
|
||||
|
||||
|
|
@ -65,6 +67,37 @@ describe('Website API tests', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Creates a website with a fixed ID.', () => {
|
||||
cy.fixture('websites').then(data => {
|
||||
const websiteCreate = data.websiteCreate;
|
||||
const fixedId = uuid();
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/websites',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: Cypress.env('authorization'),
|
||||
},
|
||||
body: { ...websiteCreate, id: fixedId },
|
||||
}).then(response => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body).to.have.property('id', fixedId);
|
||||
expect(response.body).to.have.property('name', 'Cypress Website');
|
||||
expect(response.body).to.have.property('domain', 'cypress.com');
|
||||
|
||||
// cleanup
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: `/api/websites/${fixedId}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: Cypress.env('authorization'),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns all tracked websites.', () => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
|
|
|
|||
|
|
@ -246,3 +246,38 @@ SELECT * ORDER BY toStartOfDay(created_at), website_id, referrer_domain, created
|
|||
);
|
||||
|
||||
ALTER TABLE umami.website_event MATERIALIZE PROJECTION website_event_referrer_domain_projection;
|
||||
|
||||
-- revenue
|
||||
CREATE TABLE umami.website_revenue
|
||||
(
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
event_id UUID,
|
||||
event_name String,
|
||||
currency String,
|
||||
revenue DECIMAL(18,4),
|
||||
created_at DateTime('UTC')
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(created_at)
|
||||
ORDER BY (website_id, session_id, created_at)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.website_revenue_mv
|
||||
TO umami.website_revenue
|
||||
AS
|
||||
SELECT DISTINCT
|
||||
ed.website_id,
|
||||
ed.session_id,
|
||||
ed.event_id,
|
||||
ed.event_name,
|
||||
c.currency,
|
||||
coalesce(toDecimal64(ed.number_value, 2), toDecimal64(ed.string_value, 2)) revenue,
|
||||
ed.created_at
|
||||
FROM umami.event_data ed
|
||||
JOIN (SELECT event_id, string_value as currency
|
||||
FROM umami.event_data
|
||||
WHERE positionCaseInsensitive(data_key, 'currency') > 0) c
|
||||
ON c.event_id = ed.event_id
|
||||
WHERE positionCaseInsensitive(data_key, 'revenue') > 0;
|
||||
|
|
|
|||
18
db/postgresql/migrations/11_add_segment/migration.sql
Normal file
18
db/postgresql/migrations/11_add_segment/migration.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "segment" (
|
||||
"segment_id" UUID NOT NULL,
|
||||
"website_id" UUID NOT NULL,
|
||||
"type" VARCHAR(200) NOT NULL,
|
||||
"name" VARCHAR(200) NOT NULL,
|
||||
"parameters" JSONB NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6),
|
||||
|
||||
CONSTRAINT "segment_pkey" PRIMARY KEY ("segment_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "segment_segment_id_key" ON "segment"("segment_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "segment_website_id_idx" ON "segment"("website_id");
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "report"
|
||||
ALTER COLUMN "parameters" SET DATA TYPE JSONB USING parameters::JSONB;
|
||||
28
db/postgresql/migrations/13_add_revenue/migration.sql
Normal file
28
db/postgresql/migrations/13_add_revenue/migration.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "revenue" (
|
||||
"revenue_id" UUID NOT NULL,
|
||||
"website_id" UUID NOT NULL,
|
||||
"session_id" UUID NOT NULL,
|
||||
"event_id" UUID NOT NULL,
|
||||
"event_name" VARCHAR(50) NOT NULL,
|
||||
"currency" VARCHAR(100) NOT NULL,
|
||||
"revenue" DECIMAL(19,4),
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "revenue_pkey" PRIMARY KEY ("revenue_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "revenue_revenue_id_key" ON "revenue"("revenue_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "revenue_website_id_idx" ON "revenue"("website_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "revenue_session_id_idx" ON "revenue"("session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "revenue_website_id_created_at_idx" ON "revenue"("website_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "revenue_website_id_session_id_created_at_idx" ON "revenue"("website_id", "session_id", "created_at");
|
||||
|
|
@ -43,6 +43,7 @@ model Session {
|
|||
|
||||
websiteEvent WebsiteEvent[]
|
||||
sessionData SessionData[]
|
||||
revenue Revenue[]
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([websiteId])
|
||||
|
|
@ -76,7 +77,9 @@ model Website {
|
|||
team Team? @relation(fields: [teamId], references: [id])
|
||||
eventData EventData[]
|
||||
report Report[]
|
||||
revenue Revenue[]
|
||||
sessionData SessionData[]
|
||||
segment Segment[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
|
|
@ -103,12 +106,12 @@ model WebsiteEvent {
|
|||
referrerQuery String? @map("referrer_query") @db.VarChar(500)
|
||||
referrerDomain String? @map("referrer_domain") @db.VarChar(500)
|
||||
pageTitle String? @map("page_title") @db.VarChar(500)
|
||||
gclid String? @map("gclid") @db.VarChar(255)
|
||||
fbclid String? @map("fbclid") @db.VarChar(255)
|
||||
msclkid String? @map("msclkid") @db.VarChar(255)
|
||||
ttclid String? @map("ttclid") @db.VarChar(255)
|
||||
gclid String? @db.VarChar(255)
|
||||
fbclid String? @db.VarChar(255)
|
||||
msclkid String? @db.VarChar(255)
|
||||
ttclid String? @db.VarChar(255)
|
||||
lifatid String? @map("li_fat_id") @db.VarChar(255)
|
||||
twclid String? @map("twclid") @db.VarChar(255)
|
||||
twclid String? @db.VarChar(255)
|
||||
eventType Int @default(1) @map("event_type") @db.Integer
|
||||
eventName String? @map("event_name") @db.VarChar(50)
|
||||
tag String? @db.VarChar(50)
|
||||
|
|
@ -199,7 +202,7 @@ model TeamUser {
|
|||
id String @id() @unique() @map("team_user_id") @db.Uuid
|
||||
teamId String @map("team_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
role String @map("role") @db.VarChar(50)
|
||||
role String @db.VarChar(50)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
||||
|
|
@ -215,10 +218,10 @@ model Report {
|
|||
id String @id() @unique() @map("report_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
type String @map("type") @db.VarChar(200)
|
||||
name String @map("name") @db.VarChar(200)
|
||||
description String @map("description") @db.VarChar(500)
|
||||
parameters String @map("parameters") @db.VarChar(6000)
|
||||
type String @db.VarChar(200)
|
||||
name String @db.VarChar(200)
|
||||
description String @db.VarChar(500)
|
||||
parameters Json
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
||||
|
|
@ -231,3 +234,38 @@ model Report {
|
|||
@@index([name])
|
||||
@@map("report")
|
||||
}
|
||||
|
||||
model Segment {
|
||||
id String @id() @unique() @map("segment_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
type String @db.VarChar(200)
|
||||
name String @db.VarChar(200)
|
||||
parameters Json
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
|
||||
@@index([websiteId])
|
||||
@@map("segment")
|
||||
}
|
||||
|
||||
model Revenue {
|
||||
id String @id() @unique() @map("revenue_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
sessionId String @map("session_id") @db.Uuid
|
||||
eventId String @map("event_id") @db.Uuid
|
||||
eventName String @map("event_name") @db.VarChar(50)
|
||||
currency String @db.VarChar(100)
|
||||
revenue Decimal? @db.Decimal(19, 4)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
session Session @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([websiteId])
|
||||
@@index([sessionId])
|
||||
@@index([websiteId, createdAt])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
@@map("revenue")
|
||||
}
|
||||
2660
pnpm-lock.yaml
generated
2660
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
|
@ -38,7 +38,7 @@
|
|||
"label.add-step": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add step"
|
||||
"value": "إضافة خطوة"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
|
|
@ -77,6 +77,18 @@
|
|||
"value": "تحليلات"
|
||||
}
|
||||
],
|
||||
"label.attribution": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "الإسناد"
|
||||
}
|
||||
],
|
||||
"label.attribution-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "شاهد كيف يتفاعل المستخدمون مع حملاتك التسويقية وما الذي يحفز التحويلات."
|
||||
}
|
||||
],
|
||||
"label.average": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -122,7 +134,7 @@
|
|||
"label.cancel": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "ألغِ"
|
||||
"value": "إلغاء"
|
||||
}
|
||||
],
|
||||
"label.change-password": [
|
||||
|
|
@ -152,7 +164,7 @@
|
|||
"label.compare": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Compare"
|
||||
"value": "المقارنة"
|
||||
}
|
||||
],
|
||||
"label.confirm": [
|
||||
|
|
@ -170,7 +182,7 @@
|
|||
"label.contains": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "يحتوي"
|
||||
"value": "يحتوي على"
|
||||
}
|
||||
],
|
||||
"label.continue": [
|
||||
|
|
@ -182,7 +194,7 @@
|
|||
"label.count": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Count"
|
||||
"value": "العدد"
|
||||
}
|
||||
],
|
||||
"label.countries": [
|
||||
|
|
@ -236,7 +248,7 @@
|
|||
"label.current": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Current"
|
||||
"value": "الحالي"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
|
|
@ -254,7 +266,7 @@
|
|||
"label.dashboard": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "الشاشة الرئيسية"
|
||||
"value": "لوحة التحكم"
|
||||
}
|
||||
],
|
||||
"label.data": [
|
||||
|
|
@ -356,7 +368,7 @@
|
|||
"label.does-not-contain": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "لا يحتوي"
|
||||
"value": "لا يحتوي على"
|
||||
}
|
||||
],
|
||||
"label.domain": [
|
||||
|
|
@ -374,7 +386,7 @@
|
|||
"label.edit": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "عدّل"
|
||||
"value": "تعديل"
|
||||
}
|
||||
],
|
||||
"label.edit-dashboard": [
|
||||
|
|
@ -398,13 +410,13 @@
|
|||
"label.end-step": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "End Step"
|
||||
"value": "الخطوة الأخيرة"
|
||||
}
|
||||
],
|
||||
"label.entry": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Entry URL"
|
||||
"value": "رابط الدخول"
|
||||
}
|
||||
],
|
||||
"label.event": [
|
||||
|
|
@ -428,7 +440,7 @@
|
|||
"label.exit": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Exit URL"
|
||||
"value": "رابط المغادرة"
|
||||
}
|
||||
],
|
||||
"label.false": [
|
||||
|
|
@ -476,7 +488,7 @@
|
|||
"label.first-seen": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "First seen"
|
||||
"value": "أول ظهور"
|
||||
}
|
||||
],
|
||||
"label.funnel": [
|
||||
|
|
@ -494,19 +506,19 @@
|
|||
"label.goal": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Goal"
|
||||
"value": "الهدف"
|
||||
}
|
||||
],
|
||||
"label.goals": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Goals"
|
||||
"value": "الأهداف"
|
||||
}
|
||||
],
|
||||
"label.goals-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Track your goals for pageviews and events."
|
||||
"value": "تابع تحقق أهدافك المرتبطة بمشاهدات الصفحات والأحداث."
|
||||
}
|
||||
],
|
||||
"label.greater-than": [
|
||||
|
|
@ -548,13 +560,13 @@
|
|||
"label.is": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "هو"
|
||||
"value": "يساوي"
|
||||
}
|
||||
],
|
||||
"label.is-not": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "لم"
|
||||
"value": "لا يساوي"
|
||||
}
|
||||
],
|
||||
"label.is-not-set": [
|
||||
|
|
@ -584,13 +596,13 @@
|
|||
"label.journey": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Journey"
|
||||
"value": "رحلة المستخدم"
|
||||
}
|
||||
],
|
||||
"label.journey-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Understand how users navigate through your website."
|
||||
"value": "تعرّف على كيفية تنقّل المستخدمين داخل موقعك."
|
||||
}
|
||||
],
|
||||
"label.language": [
|
||||
|
|
@ -642,7 +654,7 @@
|
|||
"label.last-months": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Last "
|
||||
"value": "آخر "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
|
|
@ -650,13 +662,13 @@
|
|||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " months"
|
||||
"value": " شهر/أشهر"
|
||||
}
|
||||
],
|
||||
"label.last-seen": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Last seen"
|
||||
"value": "آخر ظهور"
|
||||
}
|
||||
],
|
||||
"label.leave": [
|
||||
|
|
@ -704,7 +716,7 @@
|
|||
"label.manager": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manager"
|
||||
"value": "مدير"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
|
|
@ -876,13 +888,13 @@
|
|||
"label.path": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Path"
|
||||
"value": "المسار"
|
||||
}
|
||||
],
|
||||
"label.paths": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Paths"
|
||||
"value": "المسارات"
|
||||
}
|
||||
],
|
||||
"label.powered-by": [
|
||||
|
|
@ -898,19 +910,19 @@
|
|||
"label.previous": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Previous"
|
||||
"value": "السابق"
|
||||
}
|
||||
],
|
||||
"label.previous-period": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Previous period"
|
||||
"value": "الفترة السابقة"
|
||||
}
|
||||
],
|
||||
"label.previous-year": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Previous year"
|
||||
"value": "العام السابق"
|
||||
}
|
||||
],
|
||||
"label.profile": [
|
||||
|
|
@ -922,13 +934,13 @@
|
|||
"label.properties": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Properties"
|
||||
"value": "الخصائص"
|
||||
}
|
||||
],
|
||||
"label.property": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Property"
|
||||
"value": "الخاصية"
|
||||
}
|
||||
],
|
||||
"label.queries": [
|
||||
|
|
@ -1042,19 +1054,19 @@
|
|||
"label.revenue": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Revenue"
|
||||
"value": "الإيرادات"
|
||||
}
|
||||
],
|
||||
"label.revenue-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Look into your revenue across time."
|
||||
"value": "قم بإلقاء نظرة على بيانات إيراداتك وكيفية إنفاق المستخدمين."
|
||||
}
|
||||
],
|
||||
"label.revenue-property": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Revenue Property"
|
||||
"value": "خاصية الإيرادات"
|
||||
}
|
||||
],
|
||||
"label.role": [
|
||||
|
|
@ -1114,7 +1126,7 @@
|
|||
"label.session": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Session"
|
||||
"value": "الزيارة"
|
||||
}
|
||||
],
|
||||
"label.sessions": [
|
||||
|
|
@ -1144,13 +1156,13 @@
|
|||
"label.start-step": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Start Step"
|
||||
"value": "الخطوة الأولى"
|
||||
}
|
||||
],
|
||||
"label.steps": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Steps"
|
||||
"value": "الخطوات"
|
||||
}
|
||||
],
|
||||
"label.sum": [
|
||||
|
|
@ -1165,6 +1177,18 @@
|
|||
"value": "تابلت"
|
||||
}
|
||||
],
|
||||
"label.tag": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "الوسم"
|
||||
}
|
||||
],
|
||||
"label.tags": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "الوسوم"
|
||||
}
|
||||
],
|
||||
"label.team": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1180,7 +1204,7 @@
|
|||
"label.team-manager": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Team manager"
|
||||
"value": "مدير الفريق"
|
||||
}
|
||||
],
|
||||
"label.team-member": [
|
||||
|
|
@ -1204,7 +1228,7 @@
|
|||
"label.team-view-only": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Team view only"
|
||||
"value": "عرض الفريق فقط"
|
||||
}
|
||||
],
|
||||
"label.team-websites": [
|
||||
|
|
@ -1288,13 +1312,13 @@
|
|||
"label.transactions": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transactions"
|
||||
"value": "المعاملات"
|
||||
}
|
||||
],
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "نقل"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
|
|
@ -1330,7 +1354,7 @@
|
|||
"label.uniqueCustomers": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Unique Customers"
|
||||
"value": "العملاء الفريدون"
|
||||
}
|
||||
],
|
||||
"label.unknown": [
|
||||
|
|
@ -1348,19 +1372,19 @@
|
|||
"label.update": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Update"
|
||||
"value": "تحديث"
|
||||
}
|
||||
],
|
||||
"label.url": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "URL"
|
||||
"value": "الرابط"
|
||||
}
|
||||
],
|
||||
"label.urls": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "URLs"
|
||||
"value": "الروابط"
|
||||
}
|
||||
],
|
||||
"label.user": [
|
||||
|
|
@ -1372,7 +1396,7 @@
|
|||
"label.user-property": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "User Property"
|
||||
"value": "سمات المستخدم"
|
||||
}
|
||||
],
|
||||
"label.username": [
|
||||
|
|
@ -1396,7 +1420,7 @@
|
|||
"label.utm-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Track your campaigns through UTM parameters."
|
||||
"value": "تابع حملاتك التسويقية باستخدام معلمات UTM."
|
||||
}
|
||||
],
|
||||
"label.value": [
|
||||
|
|
@ -1432,7 +1456,7 @@
|
|||
"label.views-per-visit": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Views per visit"
|
||||
"value": "مشاهدات لكل زيارة"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
|
|
@ -1450,7 +1474,7 @@
|
|||
"label.visits": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visits"
|
||||
"value": "الزيارات"
|
||||
}
|
||||
],
|
||||
"label.website": [
|
||||
|
|
@ -1534,7 +1558,7 @@
|
|||
"message.collected-data": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Collected data"
|
||||
"value": "البيانات المجمعة"
|
||||
}
|
||||
],
|
||||
"message.confirm-delete": [
|
||||
|
|
@ -1754,15 +1778,7 @@
|
|||
"message.share-url": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "هذا الرابط الذي تم مشاركته بشكل عام لـ "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "target"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "."
|
||||
"value": "إحصائيات موقعك متاحة للجميع على الرابط التالي:"
|
||||
}
|
||||
],
|
||||
"message.team-already-member": [
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import WebsiteExpandedView from './WebsiteExpandedView';
|
|||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import { FILTER_COLUMNS } from '@/lib/constants';
|
||||
import { FILTER_COLUMNS, FILTER_GROUPS } from '@/lib/constants';
|
||||
|
||||
export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
||||
const pathname = usePathname();
|
||||
|
|
@ -17,7 +17,7 @@ export default function WebsiteDetailsPage({ websiteId }: { websiteId: string })
|
|||
const { view } = query;
|
||||
|
||||
const params = Object.keys(query).reduce((obj, key) => {
|
||||
if (FILTER_COLUMNS[key]) {
|
||||
if (FILTER_COLUMNS[key] || FILTER_GROUPS[key]) {
|
||||
obj[key] = query[key];
|
||||
}
|
||||
return obj;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import WebsiteHeader from '../WebsiteHeader';
|
|||
import WebsiteMetricsBar from '../WebsiteMetricsBar';
|
||||
import FilterTags from '@/components/metrics/FilterTags';
|
||||
import { useNavigation } from '@/components/hooks';
|
||||
import { FILTER_COLUMNS } from '@/lib/constants';
|
||||
import { FILTER_COLUMNS, FILTER_GROUPS } from '@/lib/constants';
|
||||
import WebsiteChart from '../WebsiteChart';
|
||||
import WebsiteCompareTables from './WebsiteCompareTables';
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export function WebsiteComparePage({ websiteId }) {
|
|||
const { query } = useNavigation();
|
||||
|
||||
const params = Object.keys(query).reduce((obj, key) => {
|
||||
if (FILTER_COLUMNS[key]) {
|
||||
if (FILTER_COLUMNS[key] || FILTER_GROUPS[key]) {
|
||||
obj[key] = query[key];
|
||||
}
|
||||
return obj;
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ repo
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
report.parameters = JSON.parse(report.parameters);
|
||||
|
||||
return json(report);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export async function POST(request: Request) {
|
|||
type,
|
||||
name,
|
||||
description,
|
||||
parameters: JSON.stringify(parameters),
|
||||
parameters: parameters,
|
||||
} as any);
|
||||
|
||||
return json(result);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { startOfHour, startOfMonth } from 'date-fns';
|
|||
import clickhouse from '@/lib/clickhouse';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { badRequest, json, forbidden, serverError } from '@/lib/response';
|
||||
import { fetchSession, fetchWebsite } from '@/lib/load';
|
||||
import { fetchWebsite } from '@/lib/load';
|
||||
import { getClientInfo, hasBlockedIp } from '@/lib/detect';
|
||||
import { createToken, parseToken } from '@/lib/jwt';
|
||||
import { secret, uuid, hash } from '@/lib/crypto';
|
||||
|
|
@ -103,32 +103,24 @@ export async function POST(request: Request) {
|
|||
|
||||
const sessionId = id ? uuid(websiteId, id) : uuid(websiteId, ip, userAgent, sessionSalt);
|
||||
|
||||
// Find session
|
||||
// Create a session if not found
|
||||
if (!clickhouse.enabled && !cache?.sessionId) {
|
||||
const session = await fetchSession(websiteId, sessionId);
|
||||
|
||||
// Create a session if not found
|
||||
if (!session) {
|
||||
try {
|
||||
await createSession({
|
||||
id: sessionId,
|
||||
websiteId,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
distinctId: id,
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (!e.message.toLowerCase().includes('unique constraint')) {
|
||||
return serverError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
await createSession(
|
||||
{
|
||||
id: sessionId,
|
||||
websiteId,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
distinctId: id,
|
||||
},
|
||||
{ skipDuplicates: true },
|
||||
);
|
||||
}
|
||||
|
||||
// Visit info
|
||||
|
|
@ -145,7 +137,8 @@ export async function POST(request: Request) {
|
|||
const base = hostname ? `https://${hostname}` : 'https://localhost';
|
||||
const currentUrl = new URL(url, base);
|
||||
|
||||
let urlPath = currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname + currentUrl.hash;
|
||||
let urlPath =
|
||||
currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname + currentUrl.hash;
|
||||
const urlQuery = currentUrl.search.substring(1);
|
||||
const urlDomain = currentUrl.hostname.replace(/^www./, '');
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export async function GET(
|
|||
}
|
||||
|
||||
const filters = {
|
||||
...getRequestFilters(query),
|
||||
...(await getRequestFilters(query)),
|
||||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export async function GET(
|
|||
const { startDate, endDate } = await getRequestDateRange(query);
|
||||
const column = FILTER_COLUMNS[type] || type;
|
||||
const filters = {
|
||||
...getRequestFilters(query),
|
||||
...(await getRequestFilters(query)),
|
||||
startDate,
|
||||
endDate,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export async function GET(
|
|||
const { startDate, endDate, unit } = await getRequestDateRange(query);
|
||||
|
||||
const filters = {
|
||||
...getRequestFilters(query),
|
||||
...(await getRequestFilters(query)),
|
||||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/lib/auth';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, notFound, ok, unauthorized } from '@/lib/response';
|
||||
import { segmentTypeParam } from '@/lib/schema';
|
||||
import { deleteSegment, getSegment, updateSegment } from '@/queries';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string; segmentId: string }> },
|
||||
) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId, segmentId } = await params;
|
||||
|
||||
const segment = await getSegment(segmentId);
|
||||
|
||||
if (websiteId && !(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
return json(segment);
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string; segmentId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
type: segmentTypeParam,
|
||||
name: z.string().max(200),
|
||||
parameters: z.object({}).passthrough(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId, segmentId } = await params;
|
||||
const { type, name, parameters } = body;
|
||||
|
||||
const segment = await getSegment(segmentId);
|
||||
|
||||
if (!segment) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const result = await updateSegment(segmentId, {
|
||||
type,
|
||||
name,
|
||||
parameters,
|
||||
} as any);
|
||||
|
||||
return json(result);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string; segmentId: string }> },
|
||||
) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId, segmentId } = await params;
|
||||
|
||||
const segment = await getSegment(segmentId);
|
||||
|
||||
if (!segment) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (!(await canDeleteWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
await deleteSegment(segmentId);
|
||||
|
||||
return ok();
|
||||
}
|
||||
67
src/app/api/websites/[websiteId]/segments/route.ts
Normal file
67
src/app/api/websites/[websiteId]/segments/route.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { canUpdateWebsite, canViewWebsite } from '@/lib/auth';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { segmentTypeParam } from '@/lib/schema';
|
||||
import { createSegment, getWebsiteSegments } from '@/queries';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
type: segmentTypeParam,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { type } = query;
|
||||
|
||||
if (websiteId && !(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const segments = await getWebsiteSegments(websiteId, type);
|
||||
|
||||
return json(segments);
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
type: segmentTypeParam,
|
||||
name: z.string().max(200),
|
||||
parameters: z.object({}).passthrough(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { type, name, parameters } = body;
|
||||
|
||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const result = await createSegment({
|
||||
id: uuid(),
|
||||
websiteId,
|
||||
type,
|
||||
name,
|
||||
parameters,
|
||||
} as any);
|
||||
|
||||
return json(result);
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ export async function GET(
|
|||
|
||||
const { startDate, endDate } = await getRequestDateRange(query);
|
||||
|
||||
const filters = getRequestFilters(query);
|
||||
const filters = await getRequestFilters(query);
|
||||
|
||||
const metrics = await getWebsiteSessionStats(websiteId, {
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export async function GET(
|
|||
endDate,
|
||||
);
|
||||
|
||||
const filters = getRequestFilters(query);
|
||||
const filters = await getRequestFilters(query);
|
||||
|
||||
const metrics = await getWebsiteStats(websiteId, {
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from '@/lib/auth';
|
||||
import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
|
||||
import { getValues } from '@/queries';
|
||||
import { parseRequest, getRequestDateRange } from '@/lib/request';
|
||||
import { EVENT_COLUMNS, FILTER_COLUMNS, FILTER_GROUPS, SESSION_COLUMNS } from '@/lib/constants';
|
||||
import { getRequestDateRange, parseRequest } from '@/lib/request';
|
||||
import { badRequest, json, unauthorized } from '@/lib/response';
|
||||
import { getWebsiteSegments, getValues } from '@/queries';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
|
|
@ -30,11 +30,17 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type)) {
|
||||
if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type) && !FILTER_GROUPS[type]) {
|
||||
return badRequest('Invalid type.');
|
||||
}
|
||||
|
||||
const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search);
|
||||
let values;
|
||||
|
||||
if (FILTER_GROUPS[type]) {
|
||||
values = (await getWebsiteSegments(websiteId, type)).map(segment => ({ value: segment.name }));
|
||||
} else {
|
||||
values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search);
|
||||
}
|
||||
|
||||
return json(values.filter(n => n).sort());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export async function POST(request: Request) {
|
|||
domain: z.string().max(500),
|
||||
shareId: z.string().max(50).nullable().optional(),
|
||||
teamId: z.string().nullable().optional(),
|
||||
id: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
|
@ -34,14 +35,14 @@ export async function POST(request: Request) {
|
|||
return error();
|
||||
}
|
||||
|
||||
const { name, domain, shareId, teamId } = body;
|
||||
const { id, name, domain, shareId, teamId } = body;
|
||||
|
||||
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data: any = {
|
||||
id: uuid(),
|
||||
id: id ?? uuid(),
|
||||
createdBy: auth.user.id,
|
||||
name,
|
||||
domain,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ export function useFields() {
|
|||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const fields = [
|
||||
// { name: 'cohort', type: 'string', label: formatMessage(labels.cohort) },
|
||||
// { name: 'segment', type: 'string', label: formatMessage(labels.segment) },
|
||||
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
|
||||
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
|
||||
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ export function useFilterParams(websiteId: string) {
|
|||
city,
|
||||
event,
|
||||
tag,
|
||||
segment,
|
||||
cohort,
|
||||
},
|
||||
} = useNavigation();
|
||||
|
||||
|
|
@ -42,5 +44,7 @@ export function useFilterParams(websiteId: string) {
|
|||
city,
|
||||
event,
|
||||
tag,
|
||||
segment,
|
||||
cohort,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ export const labels = defineMessages({
|
|||
countries: { id: 'label.countries', defaultMessage: 'Countries' },
|
||||
languages: { id: 'label.languages', defaultMessage: 'Languages' },
|
||||
tags: { id: 'label.tags', defaultMessage: 'Tags' },
|
||||
segments: { id: 'label.segments', defaultMessage: 'Segments' },
|
||||
cohorts: { id: 'label.cohorts', defaultMessage: 'Cohorts' },
|
||||
count: { id: 'label.count', defaultMessage: 'Count' },
|
||||
average: { id: 'label.average', defaultMessage: 'Average' },
|
||||
sum: { id: 'label.sum', defaultMessage: 'Sum' },
|
||||
|
|
@ -229,6 +231,8 @@ export const labels = defineMessages({
|
|||
device: { id: 'label.device', defaultMessage: 'Device' },
|
||||
pageTitle: { id: 'label.pageTitle', defaultMessage: 'Page title' },
|
||||
tag: { id: 'label.tag', defaultMessage: 'Tag' },
|
||||
segment: { id: 'label.segment', defaultMessage: 'Segment' },
|
||||
cohort: { id: 'label.cohort', defaultMessage: 'Cohort' },
|
||||
day: { id: 'label.day', defaultMessage: 'Day' },
|
||||
date: { id: 'label.date', defaultMessage: 'Date' },
|
||||
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@
|
|||
"label.add": "أضِف",
|
||||
"label.add-description": "أضِف وصف",
|
||||
"label.add-member": "أضِف عضو",
|
||||
"label.add-step": "Add step",
|
||||
"label.add-step": "إضافة خطوة",
|
||||
"label.add-website": "إضافة موقع",
|
||||
"label.admin": "مدير",
|
||||
"label.after": "يعد",
|
||||
"label.all": "الكل",
|
||||
"label.all-time": "كل الوقت",
|
||||
"label.analytics": "تحليلات",
|
||||
"label.attribution": "الإسناد",
|
||||
"label.attribution-description": "شاهد كيف يتفاعل المستخدمون مع حملاتك التسويقية وما الذي يحفز التحويلات.",
|
||||
"label.average": "المتوسط",
|
||||
"label.back": "للخلف",
|
||||
"label.before": "قبل",
|
||||
|
|
@ -19,17 +21,17 @@
|
|||
"label.breakdown": "التصنيف",
|
||||
"label.browser": "المتصفح",
|
||||
"label.browsers": "المتصفحات",
|
||||
"label.cancel": "ألغِ",
|
||||
"label.cancel": "إلغاء",
|
||||
"label.change-password": "تغيير كلمة المرور",
|
||||
"label.cities": "المدن",
|
||||
"label.city": "المدينة",
|
||||
"label.clear-all": "مسح الكل",
|
||||
"label.compare": "Compare",
|
||||
"label.compare": "المقارنة",
|
||||
"label.confirm": "تأكيد",
|
||||
"label.confirm-password": "تأكيد كلمة المرور",
|
||||
"label.contains": "يحتوي",
|
||||
"label.contains": "يحتوي على",
|
||||
"label.continue": "تابع",
|
||||
"label.count": "Count",
|
||||
"label.count": "العدد",
|
||||
"label.countries": "الدول",
|
||||
"label.country": "الدولة",
|
||||
"label.create": "أنشِئ",
|
||||
|
|
@ -38,10 +40,10 @@
|
|||
"label.create-user": "أنشِئ مستخدم",
|
||||
"label.created": "أُنشئت",
|
||||
"label.created-by": "أُنشئ من قبل",
|
||||
"label.current": "Current",
|
||||
"label.current": "الحالي",
|
||||
"label.current-password": "كلمة المرور الحالية",
|
||||
"label.custom-range": "فترة مخصّصة",
|
||||
"label.dashboard": "الشاشة الرئيسية",
|
||||
"label.dashboard": "لوحة التحكم",
|
||||
"label.data": "البيانات",
|
||||
"label.date": "التاريخ",
|
||||
"label.date-range": "فترة مخصّصة",
|
||||
|
|
@ -58,19 +60,19 @@
|
|||
"label.device": "الجهاز",
|
||||
"label.devices": "الأجهزة",
|
||||
"label.dismiss": "تجاهل",
|
||||
"label.does-not-contain": "لا يحتوي",
|
||||
"label.does-not-contain": "لا يحتوي على",
|
||||
"label.domain": "النطاق",
|
||||
"label.dropoff": "إنزال",
|
||||
"label.edit": "عدّل",
|
||||
"label.edit": "تعديل",
|
||||
"label.edit-dashboard": "عدّل لوحة التحكم",
|
||||
"label.edit-member": "عدّل العضو",
|
||||
"label.enable-share-url": "فعّل مشاركة الرابط",
|
||||
"label.end-step": "End Step",
|
||||
"label.entry": "Entry URL",
|
||||
"label.end-step": "الخطوة الأخيرة",
|
||||
"label.entry": "رابط الدخول",
|
||||
"label.event": "الحدث",
|
||||
"label.event-data": "تاريخ الحدث",
|
||||
"label.events": "الأحداث",
|
||||
"label.exit": "Exit URL",
|
||||
"label.exit": "رابط المغادرة",
|
||||
"label.false": "خطأ",
|
||||
"label.field": "الحقل",
|
||||
"label.fields": "الحقول",
|
||||
|
|
@ -78,33 +80,33 @@
|
|||
"label.filter-combined": "مُجمّعة",
|
||||
"label.filter-raw": "خام",
|
||||
"label.filters": "التصفيات",
|
||||
"label.first-seen": "First seen",
|
||||
"label.first-seen": "أول ظهور",
|
||||
"label.funnel": "قمع",
|
||||
"label.funnel-description": "فهم معدل التحويل والانقطاع عن المستخدمين.",
|
||||
"label.goal": "Goal",
|
||||
"label.goals": "Goals",
|
||||
"label.goals-description": "Track your goals for pageviews and events.",
|
||||
"label.goal": "الهدف",
|
||||
"label.goals": "الأهداف",
|
||||
"label.goals-description": "تابع تحقق أهدافك المرتبطة بمشاهدات الصفحات والأحداث.",
|
||||
"label.greater-than": "أكبَر مِن",
|
||||
"label.greater-than-equals": "أكبَر مِن أو يساوي",
|
||||
"label.host": "Host",
|
||||
"label.hosts": "Hosts",
|
||||
"label.insights": "نتائج التحليلات",
|
||||
"label.insights-description": "تعمق في بياناتك باستخدام الشرائح والتصفيات.",
|
||||
"label.is": "هو",
|
||||
"label.is-not": "لم",
|
||||
"label.is": "يساوي",
|
||||
"label.is-not": "لا يساوي",
|
||||
"label.is-not-set": "لم ضُبط",
|
||||
"label.is-set": "ضُبط",
|
||||
"label.join": "انضم",
|
||||
"label.join-team": "انضم للفريق",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.journey": "رحلة المستخدم",
|
||||
"label.journey-description": "تعرّف على كيفية تنقّل المستخدمين داخل موقعك.",
|
||||
"label.language": "اللغة",
|
||||
"label.languages": "اللغات",
|
||||
"label.laptop": "لابتوب",
|
||||
"label.last-days": "آخر {x} يوم/ايام",
|
||||
"label.last-hours": "آخر {x} ساعة",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.last-seen": "Last seen",
|
||||
"label.last-months": "آخر {x} شهر/أشهر",
|
||||
"label.last-seen": "آخر ظهور",
|
||||
"label.leave": "غادر",
|
||||
"label.leave-team": "مغادرة المجموعة",
|
||||
"label.less-than": "أقل مِن",
|
||||
|
|
@ -112,7 +114,7 @@
|
|||
"label.login": "تسجيل الدخول",
|
||||
"label.logout": "تسجيل الخروج",
|
||||
"label.manage": "التحكم",
|
||||
"label.manager": "Manager",
|
||||
"label.manager": "مدير",
|
||||
"label.max": "الحد الأقصى",
|
||||
"label.member": "عضو",
|
||||
"label.members": "الأعضاء",
|
||||
|
|
@ -134,15 +136,15 @@
|
|||
"label.pageTitle": "عنوان الصفحة",
|
||||
"label.pages": "الصفحات",
|
||||
"label.password": "كلمة المرور",
|
||||
"label.path": "Path",
|
||||
"label.paths": "Paths",
|
||||
"label.path": "المسار",
|
||||
"label.paths": "المسارات",
|
||||
"label.powered-by": "مشغل بواسطة {name}",
|
||||
"label.previous": "Previous",
|
||||
"label.previous-period": "Previous period",
|
||||
"label.previous-year": "Previous year",
|
||||
"label.previous": "السابق",
|
||||
"label.previous-period": "الفترة السابقة",
|
||||
"label.previous-year": "العام السابق",
|
||||
"label.profile": "الملف الشخصي",
|
||||
"label.properties": "Properties",
|
||||
"label.property": "Property",
|
||||
"label.properties": "الخصائص",
|
||||
"label.property": "الخاصية",
|
||||
"label.queries": "استعلامات",
|
||||
"label.query": "استعلام",
|
||||
"label.query-parameters": "متغيرات الرابط",
|
||||
|
|
@ -161,9 +163,9 @@
|
|||
"label.reset-website": "اعادة تعيين الإحصائيات",
|
||||
"label.retention": "الاحتفاظ",
|
||||
"label.retention-description": "قس مدى ثبات موقعك على الويب من خلال تتبع عدد مرات عودة المستخدمين.",
|
||||
"label.revenue": "Revenue",
|
||||
"label.revenue-description": "Look into your revenue across time.",
|
||||
"label.revenue-property": "Revenue Property",
|
||||
"label.revenue": "الإيرادات",
|
||||
"label.revenue-description": "قم بإلقاء نظرة على بيانات إيراداتك وكيفية إنفاق المستخدمين.",
|
||||
"label.revenue-property": "خاصية الإيرادات",
|
||||
"label.role": "الصلاحية",
|
||||
"label.run-query": "شغّل الاستعلام",
|
||||
"label.save": "حفظ",
|
||||
|
|
@ -173,22 +175,24 @@
|
|||
"label.select-date": "حدد التاريخ",
|
||||
"label.select-role": "حدد الدور",
|
||||
"label.select-website": "حدد موقع",
|
||||
"label.session": "Session",
|
||||
"label.session": "الزيارة",
|
||||
"label.sessions": "الزيارات",
|
||||
"label.settings": "الإعدادات",
|
||||
"label.share-url": "مشاركة الرابط",
|
||||
"label.single-day": "يوم واحد",
|
||||
"label.start-step": "Start Step",
|
||||
"label.steps": "Steps",
|
||||
"label.start-step": "الخطوة الأولى",
|
||||
"label.steps": "الخطوات",
|
||||
"label.sum": "المجموع",
|
||||
"label.tablet": "تابلت",
|
||||
"label.tag": "الوسم",
|
||||
"label.tags": "الوسوم",
|
||||
"label.team": "الفريق",
|
||||
"label.team-id": "معرّف الفريق",
|
||||
"label.team-manager": "Team manager",
|
||||
"label.team-manager": "مدير الفريق",
|
||||
"label.team-member": "عضو الفريق",
|
||||
"label.team-name": "اسم الفريق",
|
||||
"label.team-owner": "مدير الفريق",
|
||||
"label.team-view-only": "Team view only",
|
||||
"label.team-view-only": "عرض الفريق فقط",
|
||||
"label.team-websites": "مواقع الفريق",
|
||||
"label.teams": "الفرق",
|
||||
"label.theme": "السمة",
|
||||
|
|
@ -202,34 +206,34 @@
|
|||
"label.total": "الإجمالي",
|
||||
"label.total-records": "إجمالي السجلات",
|
||||
"label.tracking-code": "كود التتبع",
|
||||
"label.transactions": "Transactions",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transactions": "المعاملات",
|
||||
"label.transfer": "نقل",
|
||||
"label.transfer-website": "انقل الموقع",
|
||||
"label.true": "حقيقي",
|
||||
"label.type": "النوع",
|
||||
"label.unique": "فريد",
|
||||
"label.unique-visitors": "زائرون فريدون",
|
||||
"label.uniqueCustomers": "Unique Customers",
|
||||
"label.uniqueCustomers": "العملاء الفريدون",
|
||||
"label.unknown": "غير معروف",
|
||||
"label.untitled": "بدون عنوان",
|
||||
"label.update": "Update",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.update": "تحديث",
|
||||
"label.url": "الرابط",
|
||||
"label.urls": "الروابط",
|
||||
"label.user": "المستخدم",
|
||||
"label.user-property": "User Property",
|
||||
"label.user-property": "سمات المستخدم",
|
||||
"label.username": "اسم المستخدم",
|
||||
"label.users": "المستخدمين",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "Track your campaigns through UTM parameters.",
|
||||
"label.utm-description": "تابع حملاتك التسويقية باستخدام معلمات UTM.",
|
||||
"label.value": "القيمة",
|
||||
"label.view": "عرض",
|
||||
"label.view-details": "عرض التفاصيل",
|
||||
"label.view-only": "عرض فقط",
|
||||
"label.views": "المشاهدات",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
"label.views-per-visit": "مشاهدات لكل زيارة",
|
||||
"label.visit-duration": "متوسط وقت الزيارة",
|
||||
"label.visitors": "الزوار",
|
||||
"label.visits": "Visits",
|
||||
"label.visits": "الزيارات",
|
||||
"label.website": "الموقع",
|
||||
"label.website-id": "معرّف الموقع",
|
||||
"label.websites": "المواقع",
|
||||
|
|
@ -237,7 +241,7 @@
|
|||
"label.yesterday": "الأمس",
|
||||
"message.action-confirmation": "اكتب {confirmation} في المربع أدناه للتأكيد.",
|
||||
"message.active-users": "{x} حاليا {x, plural, one {زائر واحد} other {زوار}}",
|
||||
"message.collected-data": "Collected data",
|
||||
"message.collected-data": "البيانات المجمعة",
|
||||
"message.confirm-delete": "هل أنت متأكد من حذف {target}?",
|
||||
"message.confirm-leave": "هل أنت متأكد من مغادرة {target}?",
|
||||
"message.confirm-remove": "هل انت متأكد من حذف {target}?",
|
||||
|
|
@ -261,9 +265,9 @@
|
|||
"message.no-websites-configured": "لم تقم بإعداد اي موقع.",
|
||||
"message.page-not-found": "الصفحة غير موجودة.",
|
||||
"message.reset-website": "لإعادة ضبط موقع الويب هذا، اكتب {confirmation} في المربع أدناه للتأكيد.",
|
||||
"message.reset-website-warning": "سيتم اعادة تعيين كافة الإحصائيات لهذا الموقع، لكن لن يتم تعيير كود التتبع",
|
||||
"message.reset-website-warning": "سيتم اعادة تعيين كافة الإحصائيات لهذا الموقع، لكن لن يتم تغيير كود التتبع",
|
||||
"message.saved": "تم الحفظ بنجاح.",
|
||||
"message.share-url": "هذا الرابط الذي تم مشاركته بشكل عام لـ {target}.",
|
||||
"message.share-url": "إحصائيات موقعك متاحة للجميع على الرابط التالي:",
|
||||
"message.team-already-member": "أنت عضو في الفريق",
|
||||
"message.team-not-found": "لم يتم العثور على الفريق",
|
||||
"message.team-websites-info": "يمكن مشاهدة الموقع من اي عضو في الفريق.",
|
||||
|
|
|
|||
|
|
@ -1,279 +1,279 @@
|
|||
{
|
||||
"label.access-code": "Access code",
|
||||
"label.access-code": "Mã truy cập",
|
||||
"label.actions": "Hành động",
|
||||
"label.activity": "Activity log",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add-member": "Add member",
|
||||
"label.add-step": "Add step",
|
||||
"label.activity": "Nhật ký hoạt động",
|
||||
"label.add": "Thêm",
|
||||
"label.add-description": "Thêm mô tả",
|
||||
"label.add-member": "Thêm thành viên",
|
||||
"label.add-step": "Thêm bước",
|
||||
"label.add-website": "Thêm website",
|
||||
"label.admin": "Quản trị",
|
||||
"label.after": "After",
|
||||
"label.after": "Sau đó",
|
||||
"label.all": "Tất cả",
|
||||
"label.all-time": "Toàn thời gian",
|
||||
"label.analytics": "Analytics",
|
||||
"label.average": "Average",
|
||||
"label.back": "Quay về",
|
||||
"label.before": "Before",
|
||||
"label.analytics": "Phân tích",
|
||||
"label.average": "Trung bình",
|
||||
"label.back": "Quay lại",
|
||||
"label.before": "Trước đó",
|
||||
"label.bounce-rate": "Tỷ lệ thoát trang",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.browser": "Browser",
|
||||
"label.browsers": "Trình duyệt",
|
||||
"label.cancel": "Huỷ bỏ",
|
||||
"label.breakdown": "Phân tích chi tiết",
|
||||
"label.browser": "Trình duyệt",
|
||||
"label.browsers": "Các trình duyệt",
|
||||
"label.cancel": "Hủy bỏ",
|
||||
"label.change-password": "Đổi mật khẩu",
|
||||
"label.cities": "Cities",
|
||||
"label.city": "City",
|
||||
"label.clear-all": "Clear all",
|
||||
"label.compare": "Compare",
|
||||
"label.confirm": "Confirm",
|
||||
"label.cities": "Các thành phố",
|
||||
"label.city": "Thành phố",
|
||||
"label.clear-all": "Xóa tất cả",
|
||||
"label.compare": "So sánh",
|
||||
"label.confirm": "Xác nhận",
|
||||
"label.confirm-password": "Xác nhận mật khẩu",
|
||||
"label.contains": "Contains",
|
||||
"label.continue": "Continue",
|
||||
"label.count": "Count",
|
||||
"label.countries": "Quốc gia",
|
||||
"label.country": "Country",
|
||||
"label.create": "Create",
|
||||
"label.create-report": "Create report",
|
||||
"label.create-team": "Create team",
|
||||
"label.create-user": "Create user",
|
||||
"label.created": "Created",
|
||||
"label.created-by": "Created By",
|
||||
"label.current": "Current",
|
||||
"label.contains": "Chứa",
|
||||
"label.continue": "Tiếp tục",
|
||||
"label.count": "Số lượng",
|
||||
"label.countries": "Các quốc gia",
|
||||
"label.country": "Quốc gia",
|
||||
"label.create": "Tạo",
|
||||
"label.create-report": "Tạo báo cáo",
|
||||
"label.create-team": "Tạo nhóm",
|
||||
"label.create-user": "Tạo người dùng",
|
||||
"label.created": "Đã tạo",
|
||||
"label.created-by": "Được tạo bởi",
|
||||
"label.current": "Hiện tại",
|
||||
"label.current-password": "Mật khẩu hiện tại",
|
||||
"label.custom-range": "Phạm vi ngày tuỳ chọn",
|
||||
"label.custom-range": "Phạm vi tùy chỉnh",
|
||||
"label.dashboard": "Bảng điều khiển",
|
||||
"label.data": "Data",
|
||||
"label.date": "Date",
|
||||
"label.data": "Dữ liệu",
|
||||
"label.date": "Ngày",
|
||||
"label.date-range": "Phạm vi ngày",
|
||||
"label.day": "Day",
|
||||
"label.day": "Ngày",
|
||||
"label.default-date-range": "Khoảng thời gian mặc định",
|
||||
"label.delete": "Xoá",
|
||||
"label.delete-report": "Delete report",
|
||||
"label.delete-team": "Delete team",
|
||||
"label.delete-user": "Delete user",
|
||||
"label.delete": "Xóa",
|
||||
"label.delete-report": "Xóa báo cáo",
|
||||
"label.delete-team": "Xóa nhóm",
|
||||
"label.delete-user": "Xóa người dùng",
|
||||
"label.delete-website": "Xóa website",
|
||||
"label.description": "Description",
|
||||
"label.desktop": "Máy bàn",
|
||||
"label.details": "Details",
|
||||
"label.device": "Device",
|
||||
"label.devices": "Thiết bị",
|
||||
"label.dismiss": "Loại trừ",
|
||||
"label.does-not-contain": "Does not contain",
|
||||
"label.description": "Mô tả",
|
||||
"label.desktop": "Máy tính để bàn",
|
||||
"label.details": "Chi tiết",
|
||||
"label.device": "Thiết bị",
|
||||
"label.devices": "Các thiết bị",
|
||||
"label.dismiss": "Bỏ qua",
|
||||
"label.does-not-contain": "Không chứa",
|
||||
"label.domain": "Tên miền",
|
||||
"label.dropoff": "Dropoff",
|
||||
"label.dropoff": "Tỷ lệ bỏ qua",
|
||||
"label.edit": "Chỉnh sửa",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.edit-member": "Edit member",
|
||||
"label.enable-share-url": "Bật khả năng chia sẻ URL",
|
||||
"label.end-step": "End Step",
|
||||
"label.entry": "Entry URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Sự kiện",
|
||||
"label.exit": "Exit URL",
|
||||
"label.false": "False",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
"label.filter": "Filter",
|
||||
"label.filter-combined": "Kết hợp",
|
||||
"label.filter-raw": "Gốc",
|
||||
"label.filters": "Filters",
|
||||
"label.first-seen": "First seen",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
||||
"label.goal": "Goal",
|
||||
"label.goals": "Goals",
|
||||
"label.goals-description": "Track your goals for pageviews and events.",
|
||||
"label.greater-than": "Greater than",
|
||||
"label.greater-than-equals": "Greater than or equals",
|
||||
"label.host": "Host",
|
||||
"label.hosts": "Hosts",
|
||||
"label.insights": "Insights",
|
||||
"label.insights-description": "Dive deeper into your data by using segments and filters.",
|
||||
"label.is": "Is",
|
||||
"label.is-not": "Is not",
|
||||
"label.is-not-set": "Is not set",
|
||||
"label.is-set": "Is set",
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Ngôn ngữ",
|
||||
"label.laptop": "Laptop",
|
||||
"label.edit-dashboard": "Chỉnh sửa bảng điều khiển",
|
||||
"label.edit-member": "Chỉnh sửa thành viên",
|
||||
"label.enable-share-url": "Bật chia sẻ URL",
|
||||
"label.end-step": "Bước kết thúc",
|
||||
"label.entry": "URL truy cập",
|
||||
"label.event": "Sự kiện",
|
||||
"label.event-data": "Dữ liệu sự kiện",
|
||||
"label.events": "Các sự kiện",
|
||||
"label.exit": "URL thoát",
|
||||
"label.false": "Sai",
|
||||
"label.field": "Trường",
|
||||
"label.fields": "Các trường",
|
||||
"label.filter": "Lọc",
|
||||
"label.filter-combined": "Kết hợp lọc",
|
||||
"label.filter-raw": "Lọc thô",
|
||||
"label.filters": "Bộ lọc",
|
||||
"label.first-seen": "Lần đầu tiên nhìn thấy",
|
||||
"label.funnel": "Phễu",
|
||||
"label.funnel-description": "Tìm hiểu tỷ lệ chuyển đổi và bỏ qua của người dùng.",
|
||||
"label.goal": "Mục tiêu",
|
||||
"label.goals": "Các mục tiêu",
|
||||
"label.goals-description": "Theo dõi các mục tiêu của bạn cho lượt xem trang và sự kiện.",
|
||||
"label.greater-than": "Lớn hơn",
|
||||
"label.greater-than-equals": "Lớn hơn hoặc bằng",
|
||||
"label.host": "Máy chủ",
|
||||
"label.hosts": "Các máy chủ",
|
||||
"label.insights": "Thông tin chi tiết",
|
||||
"label.insights-description": "Tìm hiểu sâu hơn về dữ liệu của bạn bằng cách sử dụng phân đoạn và bộ lọc.",
|
||||
"label.is": "Là",
|
||||
"label.is-not": "Không phải là",
|
||||
"label.is-not-set": "Chưa được đặt",
|
||||
"label.is-set": "Đã đặt",
|
||||
"label.join": "Tham gia",
|
||||
"label.join-team": "Tham gia nhóm",
|
||||
"label.journey": "Hành trình",
|
||||
"label.journey-description": "Hiểu cách người dùng điều hướng qua website của bạn.",
|
||||
"label.language": "Ngôn ngữ",
|
||||
"label.languages": "Các ngôn ngữ",
|
||||
"label.laptop": "Máy tính xách tay",
|
||||
"label.last-days": "{x} ngày gần nhất",
|
||||
"label.last-hours": "{x} giờ gần nhất",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.last-seen": "Last seen",
|
||||
"label.leave": "Leave",
|
||||
"label.leave-team": "Leave team",
|
||||
"label.less-than": "Less than",
|
||||
"label.less-than-equals": "Less than or equals",
|
||||
"label.last-months": "{x} tháng gần nhất",
|
||||
"label.last-seen": "Lần cuối cùng nhìn thấy",
|
||||
"label.leave": "Rời khỏi",
|
||||
"label.leave-team": "Rời nhóm",
|
||||
"label.less-than": "Nhỏ hơn",
|
||||
"label.less-than-equals": "Nhỏ hơn hoặc bằng",
|
||||
"label.login": "Đăng nhập",
|
||||
"label.logout": "Đăng xuất",
|
||||
"label.manage": "Manage",
|
||||
"label.manager": "Manager",
|
||||
"label.max": "Max",
|
||||
"label.member": "Member",
|
||||
"label.members": "Members",
|
||||
"label.min": "Min",
|
||||
"label.manage": "Quản lý",
|
||||
"label.manager": "Quản lý",
|
||||
"label.max": "Tối đa",
|
||||
"label.member": "Thành viên",
|
||||
"label.members": "Các thành viên",
|
||||
"label.min": "Tối thiểu",
|
||||
"label.mobile": "Di động",
|
||||
"label.more": "Thêm",
|
||||
"label.my-account": "My account",
|
||||
"label.my-websites": "My websites",
|
||||
"label.my-account": "Tài khoản của tôi",
|
||||
"label.my-websites": "Các website của tôi",
|
||||
"label.name": "Tên",
|
||||
"label.new-password": "Mật khẩu mới",
|
||||
"label.none": "None",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||
"label.none": "Không",
|
||||
"label.number-of-records": "{x} {x, plural, one {bản ghi} other {bản ghi}}",
|
||||
"label.ok": "OK",
|
||||
"label.os": "OS",
|
||||
"label.overview": "Overview",
|
||||
"label.os": "Hệ điều hành",
|
||||
"label.overview": "Tổng quan",
|
||||
"label.owner": "Chủ sở hữu",
|
||||
"label.page-of": "Page {current} of {total}",
|
||||
"label.page-views": "Lượt xem",
|
||||
"label.pageTitle": "Page title",
|
||||
"label.pages": "Trang",
|
||||
"label.page-of": "Trang {current} trên {total}",
|
||||
"label.page-views": "Lượt xem trang",
|
||||
"label.pageTitle": "Tiêu đề trang",
|
||||
"label.pages": "Các trang",
|
||||
"label.password": "Mật khẩu",
|
||||
"label.path": "Path",
|
||||
"label.paths": "Paths",
|
||||
"label.powered-by": "Bản quyền thuộc về {name}",
|
||||
"label.previous": "Previous",
|
||||
"label.previous-period": "Previous period",
|
||||
"label.previous-year": "Previous year",
|
||||
"label.path": "Đường dẫn",
|
||||
"label.paths": "Các đường dẫn",
|
||||
"label.powered-by": "Được cung cấp bởi {name}",
|
||||
"label.previous": "Trước",
|
||||
"label.previous-period": "Kỳ trước",
|
||||
"label.previous-year": "Năm trước",
|
||||
"label.profile": "Hồ sơ",
|
||||
"label.properties": "Properties",
|
||||
"label.property": "Property",
|
||||
"label.queries": "Queries",
|
||||
"label.query": "Query",
|
||||
"label.query-parameters": "Query parameters",
|
||||
"label.properties": "Thuộc tính",
|
||||
"label.property": "Thuộc tính",
|
||||
"label.queries": "Truy vấn",
|
||||
"label.query": "Truy vấn",
|
||||
"label.query-parameters": "Tham số truy vấn",
|
||||
"label.realtime": "Thời gian thực",
|
||||
"label.referrer": "Referrer",
|
||||
"label.referrers": "Liên kết giới thiệu",
|
||||
"label.referrer": "Nguồn giới thiệu",
|
||||
"label.referrers": "Các nguồn giới thiệu",
|
||||
"label.refresh": "Làm mới",
|
||||
"label.regenerate": "Regenerate",
|
||||
"label.region": "Region",
|
||||
"label.regions": "Regions",
|
||||
"label.remove": "Remove",
|
||||
"label.remove-member": "Remove member",
|
||||
"label.reports": "Reports",
|
||||
"label.regenerate": "Tạo lại",
|
||||
"label.region": "Vùng",
|
||||
"label.regions": "Các vùng",
|
||||
"label.remove": "Xóa",
|
||||
"label.remove-member": "Xóa thành viên",
|
||||
"label.reports": "Báo cáo",
|
||||
"label.required": "Yêu cầu",
|
||||
"label.reset": "Tái thiết lập",
|
||||
"label.reset-website": "Tái thiết lập thống kê",
|
||||
"label.retention": "Retention",
|
||||
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
|
||||
"label.revenue": "Revenue",
|
||||
"label.revenue-description": "Look into your revenue across time.",
|
||||
"label.revenue-property": "Revenue Property",
|
||||
"label.role": "Role",
|
||||
"label.run-query": "Run query",
|
||||
"label.reset": "Đặt lại",
|
||||
"label.reset-website": "Đặt lại thống kê website",
|
||||
"label.retention": "Tỷ lệ giữ chân",
|
||||
"label.retention-description": "Đo lường mức độ gắn bó của website bằng cách theo dõi tần suất người dùng quay lại.",
|
||||
"label.revenue": "Doanh thu",
|
||||
"label.revenue-description": "Xem xét doanh thu của bạn theo thời gian.",
|
||||
"label.revenue-property": "Thuộc tính doanh thu",
|
||||
"label.role": "Vai trò",
|
||||
"label.run-query": "Chạy truy vấn",
|
||||
"label.save": "Lưu",
|
||||
"label.screens": "Screens",
|
||||
"label.search": "Search",
|
||||
"label.select": "Select",
|
||||
"label.select-date": "Select date",
|
||||
"label.select-role": "Select role",
|
||||
"label.select-website": "Select website",
|
||||
"label.session": "Session",
|
||||
"label.sessions": "Sessions",
|
||||
"label.screens": "Màn hình",
|
||||
"label.search": "Tìm kiếm",
|
||||
"label.select": "Chọn",
|
||||
"label.select-date": "Chọn ngày",
|
||||
"label.select-role": "Chọn vai trò",
|
||||
"label.select-website": "Chọn website",
|
||||
"label.session": "Phiên",
|
||||
"label.sessions": "Các phiên",
|
||||
"label.settings": "Cài đặt",
|
||||
"label.share-url": "Chia sẻ URL",
|
||||
"label.single-day": "Trong ngày",
|
||||
"label.start-step": "Start Step",
|
||||
"label.steps": "Steps",
|
||||
"label.sum": "Sum",
|
||||
"label.single-day": "Một ngày",
|
||||
"label.start-step": "Bước bắt đầu",
|
||||
"label.steps": "Các bước",
|
||||
"label.sum": "Tổng",
|
||||
"label.tablet": "Máy tính bảng",
|
||||
"label.team": "Team",
|
||||
"label.team-id": "Team ID",
|
||||
"label.team-manager": "Team manager",
|
||||
"label.team-member": "Team member",
|
||||
"label.team-name": "Team name",
|
||||
"label.team-owner": "Team owner",
|
||||
"label.team-view-only": "Team view only",
|
||||
"label.team-websites": "Team websites",
|
||||
"label.teams": "Teams",
|
||||
"label.theme": "Giao diện",
|
||||
"label.team": "Nhóm",
|
||||
"label.team-id": "ID nhóm",
|
||||
"label.team-manager": "Quản lý nhóm",
|
||||
"label.team-member": "Thành viên nhóm",
|
||||
"label.team-name": "Tên nhóm",
|
||||
"label.team-owner": "Chủ sở hữu nhóm",
|
||||
"label.team-view-only": "Chỉ xem nhóm",
|
||||
"label.team-websites": "Các website của nhóm",
|
||||
"label.teams": "Các nhóm",
|
||||
"label.theme": "Chủ đề",
|
||||
"label.this-month": "Tháng này",
|
||||
"label.this-week": "Tuần này",
|
||||
"label.this-year": "Năm nay",
|
||||
"label.timezone": "Múi giờ",
|
||||
"label.title": "Title",
|
||||
"label.title": "Tiêu đề",
|
||||
"label.today": "Hôm nay",
|
||||
"label.toggle-charts": "Bật/tắt biểu đồ",
|
||||
"label.total": "Total",
|
||||
"label.total-records": "Total records",
|
||||
"label.total": "Tổng",
|
||||
"label.total-records": "Tổng số bản ghi",
|
||||
"label.tracking-code": "Mã theo dõi",
|
||||
"label.transactions": "Transactions",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transfer-website": "Transfer website",
|
||||
"label.true": "True",
|
||||
"label.type": "Type",
|
||||
"label.unique": "Unique",
|
||||
"label.unique-visitors": "Khách truy cập một lần",
|
||||
"label.uniqueCustomers": "Unique Customers",
|
||||
"label.transactions": "Giao dịch",
|
||||
"label.transfer": "Chuyển giao",
|
||||
"label.transfer-website": "Chuyển giao website",
|
||||
"label.true": "Đúng",
|
||||
"label.type": "Loại",
|
||||
"label.unique": "Duy nhất",
|
||||
"label.unique-visitors": "Khách truy cập duy nhất",
|
||||
"label.uniqueCustomers": "Khách hàng duy nhất",
|
||||
"label.unknown": "Không rõ",
|
||||
"label.untitled": "Untitled",
|
||||
"label.update": "Update",
|
||||
"label.untitled": "Không có tiêu đề",
|
||||
"label.update": "Cập nhật",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.user": "User",
|
||||
"label.user-property": "User Property",
|
||||
"label.urls": "Các URL",
|
||||
"label.user": "Người dùng",
|
||||
"label.user-property": "Thuộc tính người dùng",
|
||||
"label.username": "Tên đăng nhập",
|
||||
"label.users": "Users",
|
||||
"label.users": "Người dùng",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "Track your campaigns through UTM parameters.",
|
||||
"label.value": "Value",
|
||||
"label.view": "View",
|
||||
"label.utm-description": "Theo dõi các chiến dịch của bạn thông qua các tham số UTM.",
|
||||
"label.value": "Giá trị",
|
||||
"label.view": "Xem",
|
||||
"label.view-details": "Xem chi tiết",
|
||||
"label.view-only": "View only",
|
||||
"label.views": "Xem",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
"label.visit-duration": "Thời gian truy cập trung bình",
|
||||
"label.visitors": "Khách",
|
||||
"label.visits": "Visits",
|
||||
"label.view-only": "Chỉ xem",
|
||||
"label.views": "Lượt xem",
|
||||
"label.views-per-visit": "Lượt xem trên mỗi lượt truy cập",
|
||||
"label.visit-duration": "Thời lượng truy cập",
|
||||
"label.visitors": "Khách truy cập",
|
||||
"label.visits": "Lượt truy cập",
|
||||
"label.website": "Website",
|
||||
"label.website-id": "Website ID",
|
||||
"label.websites": "Websites",
|
||||
"label.window": "Window",
|
||||
"label.yesterday": "Yesterday",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"message.active-users": "{x} hiện tại {x, plural, one {một} other {trên}}",
|
||||
"message.collected-data": "Collected data",
|
||||
"message.confirm-delete": "Bạn có chắc chắn muốn xoá {target}?",
|
||||
"message.confirm-leave": "Are you sure you want to leave {target}?",
|
||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
||||
"message.confirm-reset": "Bạn có chắc chắn muốn tái thiết lập thống kê {target}?",
|
||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
||||
"message.delete-website-warning": "Tất cả các dữ liệu liên quan cũng sẽ bị xoá.",
|
||||
"label.website-id": "ID website",
|
||||
"label.websites": "Các website",
|
||||
"label.window": "Cửa sổ",
|
||||
"label.yesterday": "Hôm qua",
|
||||
"message.action-confirmation": "Nhập {confirmation} vào ô bên dưới để xác nhận.",
|
||||
"message.active-users": "{x} {x, plural, one {người dùng} other {người dùng}} đang hoạt động",
|
||||
"message.collected-data": "Dữ liệu đã thu thập",
|
||||
"message.confirm-delete": "Bạn có chắc chắn muốn xóa {target}?",
|
||||
"message.confirm-leave": "Bạn có chắc chắn muốn rời {target}?",
|
||||
"message.confirm-remove": "Bạn có chắc chắn muốn xóa {target}?",
|
||||
"message.confirm-reset": "Bạn có chắc chắn muốn đặt lại thống kê {target}?",
|
||||
"message.delete-team-warning": "Việc xóa một nhóm cũng sẽ xóa tất cả các website của nhóm.",
|
||||
"message.delete-website-warning": "Tất cả dữ liệu liên quan cũng sẽ bị xóa.",
|
||||
"message.error": "Đã xảy ra lỗi.",
|
||||
"message.event-log": "{event} on {url}",
|
||||
"message.go-to-settings": "Chuyển tới cài đặt",
|
||||
"message.event-log": "{event} trên {url}",
|
||||
"message.go-to-settings": "Chuyển đến cài đặt",
|
||||
"message.incorrect-username-password": "Sai tên đăng nhập/mật khẩu.",
|
||||
"message.invalid-domain": "Tên miền không hợp lệ",
|
||||
"message.min-password-length": "Minimum length of {n} characters",
|
||||
"message.new-version-available": "A new version of Umami {version} is available!",
|
||||
"message.min-password-length": "Độ dài tối thiểu {n} ký tự",
|
||||
"message.new-version-available": "Có phiên bản mới của Umami {version}!",
|
||||
"message.no-data-available": "Không có dữ liệu.",
|
||||
"message.no-event-data": "No event data is available.",
|
||||
"message.no-match-password": "Mật khẩu không đồng nhất",
|
||||
"message.no-results-found": "No results were found.",
|
||||
"message.no-team-websites": "This team does not have any websites.",
|
||||
"message.no-teams": "You have not created any teams.",
|
||||
"message.no-users": "There are no users.",
|
||||
"message.no-websites-configured": "Bạn chưa có bất cứ website nào.",
|
||||
"message.page-not-found": "Trang không tìm thấy.",
|
||||
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
|
||||
"message.reset-website-warning": "Tất cả số liệu thống kê của website này sẽ bị xoá, nhưng mã theo dõi sẽ vẫn giữ nguyên.",
|
||||
"message.no-event-data": "Không có dữ liệu sự kiện.",
|
||||
"message.no-match-password": "Mật khẩu không khớp",
|
||||
"message.no-results-found": "Không tìm thấy kết quả nào.",
|
||||
"message.no-team-websites": "Nhóm này không có bất kỳ website nào.",
|
||||
"message.no-teams": "Bạn chưa tạo nhóm nào.",
|
||||
"message.no-users": "Không có người dùng nào.",
|
||||
"message.no-websites-configured": "Bạn chưa cấu hình bất kỳ website nào.",
|
||||
"message.page-not-found": "Không tìm thấy trang.",
|
||||
"message.reset-website": "Để đặt lại website này, nhập {confirmation} vào ô bên dưới để xác nhận.",
|
||||
"message.reset-website-warning": "Tất cả số liệu thống kê của website này sẽ bị xóa, nhưng mã theo dõi sẽ vẫn giữ nguyên.",
|
||||
"message.saved": "Đã lưu thành công.",
|
||||
"message.share-url": "Đây là đường dẫn URL cho {target}.",
|
||||
"message.team-already-member": "You are already a member of the team.",
|
||||
"message.team-not-found": "Team not found.",
|
||||
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
|
||||
"message.team-already-member": "Bạn đã là thành viên của nhóm.",
|
||||
"message.team-not-found": "Không tìm thấy nhóm.",
|
||||
"message.team-websites-info": "Bất kỳ ai trong nhóm đều có thể xem các website.",
|
||||
"message.tracking-code": "Mã theo dõi",
|
||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
||||
"message.triggered-event": "Triggered event",
|
||||
"message.user-deleted": "User deleted.",
|
||||
"message.viewed-page": "Viewed page",
|
||||
"message.visitor-log": "Khách từ {country} đang dùng {browser} trên {os} {device}",
|
||||
"message.visitors-dropped-off": "Visitors dropped off"
|
||||
"message.transfer-team-website-to-user": "Chuyển website này sang tài khoản của bạn?",
|
||||
"message.transfer-user-website-to-team": "Chọn nhóm để chuyển website này đến.",
|
||||
"message.transfer-website": "Chuyển quyền sở hữu website sang tài khoản của bạn hoặc một nhóm khác.",
|
||||
"message.triggered-event": "Sự kiện được kích hoạt",
|
||||
"message.user-deleted": "Người dùng đã bị xóa.",
|
||||
"message.viewed-page": "Đã xem trang",
|
||||
"message.visitor-log": "Khách từ {country} đang sử dụng {browser} trên {os} {device}",
|
||||
"message.visitors-dropped-off": "Khách truy cập đã rời đi"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,21 @@ function mapFilter(column: string, operator: string, name: string, type: string
|
|||
}
|
||||
}
|
||||
|
||||
function mapCohortFilter(column: string, operator: string, value: string) {
|
||||
switch (operator) {
|
||||
case OPERATORS.equals:
|
||||
return `${column} = '${value}'`;
|
||||
case OPERATORS.notEquals:
|
||||
return `${column} != '${value}'`;
|
||||
case OPERATORS.contains:
|
||||
return `positionCaseInsensitive(${column}, '${value}') > 0`;
|
||||
case OPERATORS.doesNotContain:
|
||||
return `positionCaseInsensitive(${column}, '${value}') = 0`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
|
||||
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
|
||||
if (column) {
|
||||
|
|
@ -105,6 +120,42 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
|
|||
return query.join('\n');
|
||||
}
|
||||
|
||||
function getCohortQuery(websiteId: string, filters: QueryFilters = {}, options: QueryOptions = {}) {
|
||||
const query = filtersToArray(filters, options).reduce(
|
||||
(arr, { name, column, operator, value }) => {
|
||||
if (column) {
|
||||
arr.push(
|
||||
`${arr.length === 0 ? 'where' : 'and'} ${mapCohortFilter(column, operator, value)}`,
|
||||
);
|
||||
|
||||
if (name === 'referrer') {
|
||||
arr.push(`and referrer_domain != hostname`);
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (query.length > 0) {
|
||||
// add website and date range filters
|
||||
query.push(`and website_id = '${websiteId}'`);
|
||||
query.push(
|
||||
`and created_at between parseDateTimeBestEffort('${filters.startDate}') and parseDateTimeBestEffort('${filters.endDate}')`,
|
||||
);
|
||||
|
||||
return `join
|
||||
(select distinct session_id
|
||||
from website_event
|
||||
${query.join('\n')}) cohort
|
||||
on cohort.session_id = website_event.session_id
|
||||
`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function getDateQuery(filters: QueryFilters = {}) {
|
||||
const { startDate, endDate, timezone } = filters;
|
||||
|
||||
|
|
@ -146,6 +197,7 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio
|
|||
websiteId,
|
||||
startDate: maxDate(filters.startDate, new Date(website?.resetAt)),
|
||||
},
|
||||
cohortQuery: getCohortQuery(websiteId, filters?.cohort),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ export const SESSION_COLUMNS = [
|
|||
'host',
|
||||
];
|
||||
|
||||
export const FILTER_GROUPS = {
|
||||
segment: 'segment',
|
||||
cohort: 'cohort',
|
||||
};
|
||||
|
||||
export const FILTER_COLUMNS = {
|
||||
url: 'url_path',
|
||||
entry: 'url_path',
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import ipaddr from 'ipaddr.js';
|
|||
import maxmind from 'maxmind';
|
||||
import {
|
||||
DESKTOP_OS,
|
||||
MOBILE_OS,
|
||||
DESKTOP_SCREEN_WIDTH,
|
||||
LAPTOP_SCREEN_WIDTH,
|
||||
MOBILE_SCREEN_WIDTH,
|
||||
IP_ADDRESS_HEADERS,
|
||||
LAPTOP_SCREEN_WIDTH,
|
||||
MOBILE_OS,
|
||||
MOBILE_SCREEN_WIDTH,
|
||||
} from './constants';
|
||||
import { safeDecodeURIComponent } from '@/lib/url';
|
||||
|
||||
const MAXMIND = 'maxmind';
|
||||
|
||||
|
|
@ -124,7 +125,9 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI
|
|||
if (!global[MAXMIND]) {
|
||||
const dir = path.join(process.cwd(), 'geo');
|
||||
|
||||
global[MAXMIND] = await maxmind.open(path.resolve(dir, 'GeoLite2-City.mmdb'));
|
||||
global[MAXMIND] = await maxmind.open(
|
||||
process.env.GEOLITE_DB_PATH || path.resolve(dir, 'GeoLite2-City.mmdb'),
|
||||
);
|
||||
}
|
||||
|
||||
// When the client IP is extracted from headers, sometimes the value includes a port
|
||||
|
|
@ -148,9 +151,9 @@ export async function getClientInfo(request: Request, payload: Record<string, an
|
|||
const userAgent = payload?.userAgent || request.headers.get('user-agent');
|
||||
const ip = payload?.ip || getIpAddress(request.headers);
|
||||
const location = await getLocation(ip, request.headers, !!payload?.ip);
|
||||
const country = location?.country;
|
||||
const region = location?.region;
|
||||
const city = location?.city;
|
||||
const country = safeDecodeURIComponent(location?.country);
|
||||
const region = safeDecodeURIComponent(location?.region);
|
||||
const city = safeDecodeURIComponent(location?.city);
|
||||
const browser = browserName(userAgent);
|
||||
const os = detectOS(userAgent) as string;
|
||||
const device = getDevice(payload?.screen, os);
|
||||
|
|
|
|||
|
|
@ -155,6 +155,24 @@ function mapFilter(column: string, operator: string, name: string, type: string
|
|||
}
|
||||
}
|
||||
|
||||
function mapCohortFilter(column: string, operator: string, value: string) {
|
||||
const db = getDatabaseType();
|
||||
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||
|
||||
switch (operator) {
|
||||
case OPERATORS.equals:
|
||||
return `${column} = '${value}'`;
|
||||
case OPERATORS.notEquals:
|
||||
return `${column} != '${value}'`;
|
||||
case OPERATORS.contains:
|
||||
return `${column} ${like} '${value}'`;
|
||||
case OPERATORS.doesNotContain:
|
||||
return `${column} not ${like} '${value}'`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string {
|
||||
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
|
||||
if (column) {
|
||||
|
|
@ -173,6 +191,43 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}):
|
|||
return query.join('\n');
|
||||
}
|
||||
|
||||
function getCohortQuery(websiteId: string, filters: QueryFilters = {}, options: QueryOptions = {}) {
|
||||
const query = filtersToArray(filters, options).reduce(
|
||||
(arr, { name, column, operator, value }) => {
|
||||
if (column) {
|
||||
arr.push(
|
||||
`${arr.length === 0 ? 'where' : 'and'} ${mapCohortFilter(column, operator, value)}`,
|
||||
);
|
||||
|
||||
if (name === 'referrer') {
|
||||
arr.push(`and referrer_domain != hostname`);
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (query.length > 0) {
|
||||
// add website and date range filters
|
||||
query.push(`and website_event.website_id = '${websiteId}'`);
|
||||
query.push(
|
||||
`and website_event.created_at between '${filters.startDate}'::timestamptz and '${filters.endDate}'::timestamptz`,
|
||||
);
|
||||
|
||||
return `join
|
||||
(select distinct website_event.session_id
|
||||
from website_event
|
||||
join session on session.session_id = website_event.session_id
|
||||
${query.join('\n')}) cohort
|
||||
on cohort.session_id = website_event.session_id
|
||||
`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function getDateQuery(filters: QueryFilters = {}) {
|
||||
const { startDate, endDate } = filters;
|
||||
|
||||
|
|
@ -219,6 +274,7 @@ async function parseFilters(
|
|||
websiteId,
|
||||
startDate: maxDate(filters.startDate, website?.resetAt),
|
||||
},
|
||||
cohortQuery: getCohortQuery(websiteId, filters?.cohort),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { z, ZodSchema } from 'zod';
|
||||
import { FILTER_COLUMNS } from '@/lib/constants';
|
||||
import { FILTER_COLUMNS, FILTER_GROUPS } from '@/lib/constants';
|
||||
import { badRequest, unauthorized } from '@/lib/response';
|
||||
import { getAllowedUnits, getMinimumUnit } from '@/lib/date';
|
||||
import { checkAuth } from '@/lib/auth';
|
||||
import { getWebsiteDateRange } from '@/queries';
|
||||
import { getWebsiteSegment, getWebsiteDateRange } from '@/queries';
|
||||
|
||||
export async function getJsonBody(request: Request) {
|
||||
try {
|
||||
|
|
@ -85,14 +85,28 @@ export async function getRequestDateRange(query: Record<string, any>) {
|
|||
};
|
||||
}
|
||||
|
||||
export function getRequestFilters(query: Record<string, any>) {
|
||||
return Object.keys(FILTER_COLUMNS).reduce((obj, key) => {
|
||||
export async function getRequestFilters(query: Record<string, any>, websiteId?: string) {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
for (const key of Object.keys(FILTER_COLUMNS)) {
|
||||
const value = query[key];
|
||||
|
||||
if (value !== undefined) {
|
||||
obj[key] = value;
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
for (const key of Object.keys(FILTER_GROUPS)) {
|
||||
const value = query[key];
|
||||
if (value !== undefined) {
|
||||
const segment = await getWebsiteSegment(websiteId, key, value);
|
||||
if (key === 'segment') {
|
||||
// merge filters into result
|
||||
Object.assign(result, segment.parameters);
|
||||
} else {
|
||||
result[key] = segment.parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export const filterParams = {
|
|||
host: z.string().optional(),
|
||||
language: z.string().optional(),
|
||||
event: z.string().optional(),
|
||||
segment: z.string().optional(),
|
||||
cohort: z.string().optional(),
|
||||
};
|
||||
|
||||
export const pagingParams = {
|
||||
|
|
@ -74,3 +76,5 @@ export const reportParms = {
|
|||
value: z.string().optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
export const segmentTypeParam = z.enum(['segment', 'cohort']);
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ export interface QueryFilters {
|
|||
event?: string;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
cohort?: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface QueryOptions {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from '@/queries/prisma/report';
|
||||
export * from '@/queries/prisma/segment';
|
||||
export * from '@/queries/prisma/team';
|
||||
export * from '@/queries/prisma/teamUser';
|
||||
export * from '@/queries/prisma/user';
|
||||
|
|
|
|||
45
src/queries/prisma/segment.ts
Normal file
45
src/queries/prisma/segment.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import prisma from '@/lib/prisma';
|
||||
import { Prisma, Segment } from '@prisma/client';
|
||||
|
||||
async function findSegment(criteria: Prisma.SegmentFindUniqueArgs): Promise<Segment> {
|
||||
return prisma.client.Segment.findUnique(criteria);
|
||||
}
|
||||
|
||||
export async function getSegment(segmentId: string): Promise<Segment> {
|
||||
return findSegment({
|
||||
where: {
|
||||
id: segmentId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWebsiteSegment(
|
||||
websiteId: string,
|
||||
type: string,
|
||||
name: string,
|
||||
): Promise<Segment> {
|
||||
return prisma.client.segment.findFirst({
|
||||
where: { websiteId, type, name },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWebsiteSegments(websiteId: string, type: string): Promise<Segment[]> {
|
||||
return prisma.client.Segment.findMany({
|
||||
where: { websiteId, type },
|
||||
});
|
||||
}
|
||||
|
||||
export async function createSegment(data: Prisma.SegmentUncheckedCreateInput): Promise<Segment> {
|
||||
return prisma.client.Segment.create({ data });
|
||||
}
|
||||
|
||||
export async function updateSegment(
|
||||
SegmentId: string,
|
||||
data: Prisma.SegmentUpdateInput,
|
||||
): Promise<Segment> {
|
||||
return prisma.client.Segment.update({ where: { id: SegmentId }, data });
|
||||
}
|
||||
|
||||
export async function deleteSegment(SegmentId: string): Promise<Segment> {
|
||||
return prisma.client.Segment.delete({ where: { id: SegmentId } });
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ export async function getEventDataFields(
|
|||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery, parseFilters, getDateSQL } = prisma;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -29,6 +29,9 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
count(*) as "total"
|
||||
from event_data
|
||||
join website_event on website_event.event_id = event_data.website_event_id
|
||||
and website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${cohortQuery}
|
||||
where event_data.website_id = {{websiteId::uuid}}
|
||||
and event_data.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
|
|
@ -45,7 +48,7 @@ async function clickhouseQuery(
|
|||
filters: QueryFilters,
|
||||
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -56,7 +59,8 @@ async function clickhouseQuery(
|
|||
data_type = 4, toString(date_trunc('hour', date_value)),
|
||||
string_value) as "value",
|
||||
count(*) as "total"
|
||||
from event_data
|
||||
from event_data website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ async function relationalQuery(
|
|||
filters: QueryFilters & { propertyName?: string },
|
||||
) {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
|
||||
columns: { propertyName: 'data_key' },
|
||||
});
|
||||
|
||||
|
|
@ -29,6 +29,9 @@ async function relationalQuery(
|
|||
count(*) as "total"
|
||||
from event_data
|
||||
join website_event on website_event.event_id = event_data.website_event_id
|
||||
and website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${cohortQuery}
|
||||
where event_data.website_id = {{websiteId::uuid}}
|
||||
and event_data.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
|
|
@ -45,7 +48,7 @@ async function clickhouseQuery(
|
|||
filters: QueryFilters & { propertyName?: string },
|
||||
): Promise<{ eventName: string; propertyName: string; total: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
|
||||
columns: { propertyName: 'data_key' },
|
||||
});
|
||||
|
||||
|
|
@ -55,7 +58,8 @@ async function clickhouseQuery(
|
|||
event_name as eventName,
|
||||
data_key as propertyName,
|
||||
count(*) as total
|
||||
from event_data
|
||||
from event_data website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function getEventDataStats(
|
|||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -32,8 +32,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
data_key,
|
||||
count(*) as "total"
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
join website_event on website_event.event_id = event_data.website_event_id
|
||||
and website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${cohortQuery}
|
||||
where event_data.website_id = {{websiteId::uuid}}
|
||||
and event_data.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
group by website_event_id, data_key
|
||||
) as t
|
||||
|
|
@ -47,7 +51,7 @@ async function clickhouseQuery(
|
|||
filters: QueryFilters,
|
||||
): Promise<{ events: number; properties: number; records: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -60,7 +64,8 @@ async function clickhouseQuery(
|
|||
event_id,
|
||||
data_key,
|
||||
count(*) as "total"
|
||||
from event_data
|
||||
from event_data website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ async function relationalQuery(
|
|||
filters: QueryFilters & { eventName?: string; propertyName?: string },
|
||||
) {
|
||||
const { rawQuery, parseFilters, getDateSQL } = prisma;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -33,6 +33,9 @@ async function relationalQuery(
|
|||
count(*) as "total"
|
||||
from event_data
|
||||
join website_event on website_event.event_id = event_data.website_event_id
|
||||
and website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${cohortQuery}
|
||||
where event_data.website_id = {{websiteId::uuid}}
|
||||
and event_data.created_at between {{startDate}} and {{endDate}}
|
||||
and event_data.data_key = {{propertyName}}
|
||||
|
|
@ -51,7 +54,7 @@ async function clickhouseQuery(
|
|||
filters: QueryFilters & { eventName?: string; propertyName?: string },
|
||||
): Promise<{ value: string; total: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -60,7 +63,8 @@ async function clickhouseQuery(
|
|||
data_type = 4, toString(date_trunc('hour', date_value)),
|
||||
string_value) as "value",
|
||||
count(*) as "total"
|
||||
from event_data
|
||||
from event_data website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and data_key = {propertyName:String}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export async function getEventMetrics(
|
|||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { rawQuery, getDateSQL, parseFilters } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.customEvent,
|
||||
});
|
||||
|
|
@ -28,6 +28,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
${getDateSQL('website_event.created_at', unit, timezone)} t,
|
||||
count(*) y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSession}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
|
@ -46,20 +47,21 @@ async function clickhouseQuery(
|
|||
): Promise<{ x: string; t: string; y: number }[]> {
|
||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
||||
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.customEvent,
|
||||
});
|
||||
|
||||
let sql = '';
|
||||
|
||||
if (filterQuery) {
|
||||
if (filterQuery || cohortQuery) {
|
||||
sql = `
|
||||
select
|
||||
event_name x,
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
count(*) y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function getWebsiteEvents(
|
|||
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
|
||||
const { pagedRawQuery, parseFilters } = prisma;
|
||||
const { search } = pageParams;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
});
|
||||
|
||||
|
|
@ -24,7 +24,6 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
|
||||
return pagedRawQuery(
|
||||
`
|
||||
with events as (
|
||||
select
|
||||
event_id as "id",
|
||||
website_id as "websiteId",
|
||||
|
|
@ -39,6 +38,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
event_type as "eventType",
|
||||
event_name as "eventName"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
|
|
@ -49,8 +49,6 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
: ''
|
||||
}
|
||||
order by created_at desc
|
||||
limit 1000)
|
||||
select * from events
|
||||
`,
|
||||
{ ...params, search: `%${search}%` },
|
||||
pageParams,
|
||||
|
|
@ -59,12 +57,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
|
||||
const { pagedQuery, parseFilters } = clickhouse;
|
||||
const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
|
||||
const { params, dateQuery, filterQuery, cohortQuery } = await parseFilters(websiteId, filters);
|
||||
const { search } = pageParams;
|
||||
|
||||
return pagedQuery(
|
||||
`
|
||||
with events as (
|
||||
select
|
||||
event_id as id,
|
||||
website_id as websiteId,
|
||||
|
|
@ -79,6 +76,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
event_type as eventType,
|
||||
event_name as eventName
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
|
|
@ -89,8 +87,6 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
: ''
|
||||
}
|
||||
order by created_at desc
|
||||
limit 1000)
|
||||
select * from events
|
||||
`,
|
||||
{ ...params, search },
|
||||
pageParams,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import kafka from '@/lib/kafka';
|
|||
import prisma from '@/lib/prisma';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { saveEventData } from './saveEventData';
|
||||
import { saveRevenue } from './saveRevenue';
|
||||
|
||||
export interface SaveEventArgs {
|
||||
websiteId: string;
|
||||
|
|
@ -130,6 +131,20 @@ async function relationalQuery({
|
|||
eventData,
|
||||
createdAt,
|
||||
});
|
||||
|
||||
const { revenue, currency } = eventData;
|
||||
|
||||
if (revenue > 0 && currency) {
|
||||
await saveRevenue({
|
||||
websiteId,
|
||||
sessionId,
|
||||
eventId: websiteEventId,
|
||||
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||
currency,
|
||||
revenue,
|
||||
createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
36
src/queries/sql/events/saveRevenue.ts
Normal file
36
src/queries/sql/events/saveRevenue.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { uuid } from '@/lib/crypto';
|
||||
import { PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export interface SaveRevenueArgs {
|
||||
websiteId: string;
|
||||
sessionId: string;
|
||||
eventId: string;
|
||||
eventName: string;
|
||||
currency: string;
|
||||
revenue: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export async function saveRevenue(data: SaveRevenueArgs) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(data),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(data: SaveRevenueArgs) {
|
||||
const { websiteId, sessionId, eventId, eventName, currency, revenue, createdAt } = data;
|
||||
|
||||
await prisma.client.revenue.create({
|
||||
data: {
|
||||
id: uuid(),
|
||||
websiteId,
|
||||
sessionId,
|
||||
eventId,
|
||||
eventName,
|
||||
currency,
|
||||
revenue,
|
||||
createdAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ export async function getChannelMetrics(...args: [websiteId: string, filters?: Q
|
|||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -21,6 +21,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
url_query as query,
|
||||
count(distinct session_id) as visitors
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {{websiteId::uuid}}
|
||||
${filterQuery}
|
||||
${dateQuery}
|
||||
|
|
@ -36,7 +37,7 @@ async function clickhouseQuery(
|
|||
filters: QueryFilters,
|
||||
): Promise<{ x: string; y: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||
|
||||
const sql = `
|
||||
select
|
||||
|
|
@ -44,6 +45,7 @@ async function clickhouseQuery(
|
|||
url_query as query,
|
||||
uniq(session_id) as visitors
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
${filterQuery}
|
||||
${dateQuery}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export async function getRealtimeActivity(...args: [websiteId: string, filters:
|
|||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -27,6 +27,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
website_event.url_path as "urlPath",
|
||||
website_event.referrer_domain as "referrerDomain"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
inner join session
|
||||
on session.session_id = website_event.session_id
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
|
|
@ -41,7 +42,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -56,6 +57,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
|
|||
url_path as urlPath,
|
||||
referrer_domain as referrerDomain
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
${filterQuery}
|
||||
${dateQuery}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ async function relationalQuery(
|
|||
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
|
||||
> {
|
||||
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -44,6 +44,7 @@ async function relationalQuery(
|
|||
min(website_event.created_at) as "min_time",
|
||||
max(website_event.created_at) as "max_time"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSession}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
|
@ -63,7 +64,7 @@ async function clickhouseQuery(
|
|||
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
|
||||
> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -86,6 +87,7 @@ async function clickhouseQuery(
|
|||
min(created_at) min_time,
|
||||
max(created_at) max_time
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
@ -108,6 +110,7 @@ async function clickhouseQuery(
|
|||
min(min_time) min_time,
|
||||
max(max_time) max_time
|
||||
from umami.website_event_stats_hourly "website_event"
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ async function relationalQuery(
|
|||
) {
|
||||
const column = FILTER_COLUMNS[type] || type;
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(
|
||||
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
|
||||
websiteId,
|
||||
{
|
||||
...filters,
|
||||
|
|
@ -68,6 +68,7 @@ async function relationalQuery(
|
|||
select ${column} x,
|
||||
${column === 'referrer_domain' ? 'count(distinct website_event.session_id)' : 'count(*)'} as y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSession}
|
||||
${entryExitQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
|
|
@ -93,7 +94,7 @@ async function clickhouseQuery(
|
|||
): Promise<{ x: string; y: number }[]> {
|
||||
const column = FILTER_COLUMNS[type] || type;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -127,6 +128,7 @@ async function clickhouseQuery(
|
|||
select ${column} x,
|
||||
${column === 'referrer_domain' ? 'uniq(session_id)' : 'count(*)'} as y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${entryExitQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
|
|
@ -165,6 +167,7 @@ async function clickhouseQuery(
|
|||
from (
|
||||
select ${columnQuery} as t
|
||||
from website_event_stats_hourly website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export async function getPageviewStats(...args: [websiteId: string, filters: Que
|
|||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { getDateSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -25,6 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
${getDateSQL('website_event.created_at', unit, timezone)} x,
|
||||
count(*) y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSession}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
|
@ -43,7 +44,7 @@ async function clickhouseQuery(
|
|||
): Promise<{ x: string; y: number }[]> {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -60,6 +61,7 @@ async function clickhouseQuery(
|
|||
${getDateSQL('website_event.created_at', unit, timezone)} as t,
|
||||
count(*) as y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
@ -78,6 +80,7 @@ async function clickhouseQuery(
|
|||
${getDateSQL('website_event.created_at', unit, timezone)} as t,
|
||||
sum(views)as y
|
||||
from website_event_stats_hourly website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -78,26 +78,16 @@ async function relationalQuery(
|
|||
group by 1),`;
|
||||
|
||||
const revenueEventQuery = `WITH events AS (
|
||||
select
|
||||
we.session_id,
|
||||
max(ed.created_at) max_dt,
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) value
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
and we.website_id = ed.website_id
|
||||
join (select website_event_id
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and data_key ${like} '%currency%'
|
||||
and string_value = {{currency}}) currency
|
||||
on currency.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ${column} = {{conversionStep}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
group by 1),`;
|
||||
select
|
||||
session_id,
|
||||
max(created_at) max_dt,
|
||||
sum(revenue) value
|
||||
from revenue
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and ${column} = {{conversionStep}}
|
||||
and currency ${like} {{currency}}
|
||||
group by 1),`;
|
||||
|
||||
function getModelQuery(model: string) {
|
||||
return model === 'firstClick'
|
||||
|
|
@ -311,21 +301,14 @@ async function clickhouseQuery(
|
|||
|
||||
const revenueEventQuery = `WITH events AS (
|
||||
select
|
||||
ed.session_id,
|
||||
max(ed.created_at) max_dt,
|
||||
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) as value
|
||||
from event_data ed
|
||||
join (select event_id
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||
and string_value = {currency:String}) c
|
||||
on c.event_id = ed.event_id
|
||||
session_id,
|
||||
max(created_at) max_dt,
|
||||
sum(revenue) as value
|
||||
from website_revenue
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and ${column} = {conversionStep:String}
|
||||
and positionCaseInsensitive(ed.data_key, 'revenue') > 0
|
||||
and ${column} = {conversionStep:String}
|
||||
and currency = {currency:String}
|
||||
group by 1),`;
|
||||
|
||||
function getModelQuery(model: string) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ async function relationalQuery(
|
|||
}[]
|
||||
> {
|
||||
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(
|
||||
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
|
||||
websiteId,
|
||||
{
|
||||
...filters,
|
||||
|
|
@ -53,6 +53,7 @@ async function relationalQuery(
|
|||
min(website_event.created_at) as "min_time",
|
||||
max(website_event.created_at) as "max_time"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSession}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
|
@ -80,7 +81,7 @@ async function clickhouseQuery(
|
|||
}[]
|
||||
> {
|
||||
const { parseFilters, rawQuery } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -103,6 +104,7 @@ async function clickhouseQuery(
|
|||
min(created_at) min_time,
|
||||
max(created_at) max_time
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -48,22 +48,13 @@ async function relationalQuery(
|
|||
const chartRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
we.event_name x,
|
||||
${getDateSQL('ed.created_at', unit, timezone)} t,
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) y
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
join (select website_event_id
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and data_key ${like} '%currency%'
|
||||
and string_value = {{currency}}) currency
|
||||
on currency.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
event_name x,
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
sum(revenue) y
|
||||
from revenue
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and currency ${like} {{currency}}
|
||||
group by x, t
|
||||
order by t
|
||||
`,
|
||||
|
|
@ -74,22 +65,13 @@ async function relationalQuery(
|
|||
`
|
||||
select
|
||||
s.country as name,
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) value
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
sum(r.revenue) value
|
||||
from revenue r
|
||||
join session s
|
||||
on s.session_id = we.session_id
|
||||
join (select website_event_id
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and data_key ${like} '%currency%'
|
||||
and string_value = {{currency}}) currency
|
||||
on currency.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
on s.session_id = r.session_id
|
||||
where r.website_id = {{websiteId::uuid}}
|
||||
and r.created_at between {{startDate}} and {{endDate}}
|
||||
and r.currency ${like} {{currency}}
|
||||
group by s.country
|
||||
`,
|
||||
{ websiteId, startDate, endDate, currency },
|
||||
|
|
@ -98,22 +80,13 @@ async function relationalQuery(
|
|||
const totalRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) as sum,
|
||||
sum(revenue) as sum,
|
||||
count(distinct event_id) as count,
|
||||
count(distinct session_id) as unique_count
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
join (select website_event_id
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and data_key ${like} '%currency%'
|
||||
and string_value = {{currency}}) currency
|
||||
on currency.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
from revenue r
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and currency ${like} {{currency}}
|
||||
`,
|
||||
{ websiteId, startDate, endDate, currency },
|
||||
).then(result => result?.[0]);
|
||||
|
|
@ -121,24 +94,15 @@ async function relationalQuery(
|
|||
const tableRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
c.currency,
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) as sum,
|
||||
count(distinct ed.website_event_id) as count,
|
||||
count(distinct we.session_id) as unique_count
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
join (select website_event_id, string_value as currency
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and data_key ${like} '%currency%') c
|
||||
on c.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
group by c.currency
|
||||
order by sum desc;
|
||||
currency,
|
||||
sum(revenue) as sum,
|
||||
count(distinct event_id) as count,
|
||||
count(distinct session_id) as unique_count
|
||||
from revenue r
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
group by currency
|
||||
order by sum desc
|
||||
`,
|
||||
{ websiteId, startDate, endDate, unit, timezone, currency },
|
||||
);
|
||||
|
|
@ -180,18 +144,11 @@ async function clickhouseQuery(
|
|||
select
|
||||
event_name x,
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) y
|
||||
from event_data
|
||||
join (select event_id
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||
and string_value = {currency:String}) currency
|
||||
on currency.event_id = event_data.event_id
|
||||
sum(revenue) y
|
||||
from website_revenue
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'revenue') > 0
|
||||
and currency = {currency:String}
|
||||
group by x, t
|
||||
order by t
|
||||
`,
|
||||
|
|
@ -207,23 +164,16 @@ async function clickhouseQuery(
|
|||
`
|
||||
select
|
||||
s.country as name,
|
||||
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) as value
|
||||
from event_data ed
|
||||
join (select event_id
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||
and string_value = {currency:String}) c
|
||||
on c.event_id = ed.event_id
|
||||
sum(w.revenue) as value
|
||||
from website_revenue w
|
||||
join (select distinct website_id, session_id, country
|
||||
from website_event_stats_hourly
|
||||
where website_id = {websiteId:UUID}) s
|
||||
on ed.website_id = s.website_id
|
||||
and ed.session_id = s.session_id
|
||||
where ed.website_id = {websiteId:UUID}
|
||||
and ed.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(ed.data_key, 'revenue') > 0
|
||||
on w.website_id = s.website_id
|
||||
and w.session_id = s.session_id
|
||||
where w.website_id = {websiteId:UUID}
|
||||
and w.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and w.currency = {currency:String}
|
||||
group by s.country
|
||||
`,
|
||||
{ websiteId, startDate, endDate, currency },
|
||||
|
|
@ -237,20 +187,13 @@ async function clickhouseQuery(
|
|||
}>(
|
||||
`
|
||||
select
|
||||
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) as sum,
|
||||
sum(revenue) as sum,
|
||||
uniqExact(event_id) as count,
|
||||
uniqExact(session_id) as unique_count
|
||||
from event_data
|
||||
join (select event_id
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||
and string_value = {currency:String}) currency
|
||||
on currency.event_id = event_data.event_id
|
||||
from website_revenue
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'revenue') > 0
|
||||
and currency = {currency:String}
|
||||
`,
|
||||
{ websiteId, startDate, endDate, currency },
|
||||
).then(result => result?.[0]);
|
||||
|
|
@ -266,22 +209,15 @@ async function clickhouseQuery(
|
|||
>(
|
||||
`
|
||||
select
|
||||
c.currency,
|
||||
sum(coalesce(toDecimal64(ed.number_value, 2), toDecimal64(ed.string_value, 2))) as sum,
|
||||
uniqExact(ed.event_id) as count,
|
||||
uniqExact(ed.session_id) as unique_count
|
||||
from event_data ed
|
||||
join (select event_id, string_value as currency
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'currency') > 0) c
|
||||
on c.event_id = ed.event_id
|
||||
currency,
|
||||
sum(revenue) as sum,
|
||||
uniqExact(event_id) as count,
|
||||
uniqExact(session_id) as unique_count
|
||||
from website_revenue
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'revenue') > 0
|
||||
group by c.currency
|
||||
order by sum desc;
|
||||
group by currency
|
||||
order by sum desc
|
||||
`,
|
||||
{ websiteId, startDate, endDate, unit, timezone, currency },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { Prisma } from '@prisma/client';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function createSession(data: Prisma.SessionCreateInput) {
|
||||
export async function createSession(
|
||||
data: Prisma.SessionCreateInput,
|
||||
options = { skipDuplicates: false },
|
||||
) {
|
||||
const {
|
||||
id,
|
||||
websiteId,
|
||||
|
|
@ -16,19 +19,31 @@ export async function createSession(data: Prisma.SessionCreateInput) {
|
|||
distinctId,
|
||||
} = data;
|
||||
|
||||
return prisma.client.session.create({
|
||||
data: {
|
||||
id,
|
||||
websiteId,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
distinctId,
|
||||
},
|
||||
});
|
||||
try {
|
||||
return await prisma.client.session.create({
|
||||
data: {
|
||||
id,
|
||||
websiteId,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
distinctId,
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
// With skipDuplicates flag: ignore unique constraint error and return null
|
||||
if (
|
||||
options.skipDuplicates &&
|
||||
e instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
e.code === 'P2002'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ async function relationalQuery(
|
|||
filters: QueryFilters & { propertyName?: string },
|
||||
) {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
|
||||
columns: { propertyName: 'data_key' },
|
||||
});
|
||||
|
||||
|
|
@ -25,12 +25,13 @@ async function relationalQuery(
|
|||
`
|
||||
select
|
||||
data_key as "propertyName",
|
||||
count(distinct d.session_id) as "total"
|
||||
from website_event e
|
||||
join session_data d
|
||||
on d.session_id = e.session_id
|
||||
where e.website_id = {{websiteId::uuid}}
|
||||
and e.created_at between {{startDate}} and {{endDate}}
|
||||
count(distinct session_data.session_id) as "total"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
join session_data
|
||||
on session_data.session_id = website_event.session_id
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
group by 1
|
||||
order by 2 desc
|
||||
|
|
@ -45,7 +46,7 @@ async function clickhouseQuery(
|
|||
filters: QueryFilters & { propertyName?: string },
|
||||
): Promise<{ propertyName: string; total: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
|
||||
columns: { propertyName: 'data_key' },
|
||||
});
|
||||
|
||||
|
|
@ -53,13 +54,14 @@ async function clickhouseQuery(
|
|||
`
|
||||
select
|
||||
data_key as propertyName,
|
||||
count(distinct d.session_id) as total
|
||||
from website_event e
|
||||
join session_data d final
|
||||
on d.session_id = e.session_id
|
||||
where e.website_id = {websiteId:UUID}
|
||||
and e.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and d.data_key != ''
|
||||
count(distinct session_data.session_id) as total
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
join session_data final
|
||||
on session_data.session_id = website_event.session_id
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and session_data.data_key != ''
|
||||
${filterQuery}
|
||||
group by 1
|
||||
order by 2 desc
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ async function relationalQuery(
|
|||
filters: QueryFilters & { propertyName?: string },
|
||||
) {
|
||||
const { rawQuery, parseFilters, getDateSQL } = prisma;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -27,13 +27,14 @@ async function relationalQuery(
|
|||
when data_type = 4 then ${getDateSQL('date_value', 'hour')}
|
||||
else string_value
|
||||
end as "value",
|
||||
count(distinct d.session_id) as "total"
|
||||
count(distinct session_data.session_id) as "total"
|
||||
from website_event e
|
||||
${cohortQuery}
|
||||
join session_data d
|
||||
on d.session_id = e.session_id
|
||||
where e.website_id = {{websiteId::uuid}}
|
||||
and e.created_at between {{startDate}} and {{endDate}}
|
||||
and d.data_key = {{propertyName}}
|
||||
on session_data.session_id = website_event.session_id
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and session_data.data_key = {{propertyName}}
|
||||
${filterQuery}
|
||||
group by value
|
||||
order by 2 desc
|
||||
|
|
@ -48,7 +49,7 @@ async function clickhouseQuery(
|
|||
filters: QueryFilters & { propertyName?: string },
|
||||
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -56,13 +57,14 @@ async function clickhouseQuery(
|
|||
multiIf(data_type = 2, replaceAll(string_value, '.0000', ''),
|
||||
data_type = 4, toString(date_trunc('hour', date_value)),
|
||||
string_value) as "value",
|
||||
uniq(d.session_id) as "total"
|
||||
uniq(session_data.session_id) as "total"
|
||||
from website_event e
|
||||
${cohortQuery}
|
||||
join session_data d final
|
||||
on d.session_id = e.session_id
|
||||
where e.website_id = {websiteId:UUID}
|
||||
and e.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and d.data_key = {propertyName:String}
|
||||
on session_data.session_id = website_event.session_id
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and session_data.data_key = {propertyName:String}
|
||||
${filterQuery}
|
||||
group by value
|
||||
order by 2 desc
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ async function relationalQuery(
|
|||
) {
|
||||
const column = FILTER_COLUMNS[type] || type;
|
||||
const { parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(
|
||||
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
|
||||
websiteId,
|
||||
{
|
||||
...filters,
|
||||
|
|
@ -47,6 +47,7 @@ async function relationalQuery(
|
|||
count(distinct website_event.session_id) y
|
||||
${includeCountry ? ', country' : ''}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSession}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
|
@ -71,7 +72,7 @@ async function clickhouseQuery(
|
|||
): Promise<{ x: string; y: number }[]> {
|
||||
const column = FILTER_COLUMNS[type] || type;
|
||||
const { parseFilters, rawQuery } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -86,6 +87,7 @@ async function clickhouseQuery(
|
|||
count(distinct session_id) y
|
||||
${includeCountry ? ', country' : ''}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
@ -103,6 +105,7 @@ async function clickhouseQuery(
|
|||
uniq(session_id) y
|
||||
${includeCountry ? ', country' : ''}
|
||||
from website_event_stats_hourly website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer
|
|||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { getDateSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -25,6 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
${getDateSQL('website_event.created_at', unit, timezone)} x,
|
||||
count(distinct website_event.session_id) y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSession}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
|
@ -43,7 +44,7 @@ async function clickhouseQuery(
|
|||
): Promise<{ x: string; y: number }[]> {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
});
|
||||
|
|
@ -60,6 +61,7 @@ async function clickhouseQuery(
|
|||
${getDateSQL('website_event.created_at', unit, timezone)} as t,
|
||||
count(distinct session_id) as y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
@ -78,6 +80,7 @@ async function clickhouseQuery(
|
|||
${getDateSQL('website_event.created_at', unit, timezone)} as t,
|
||||
uniq(session_id) as y
|
||||
from website_event_stats_hourly website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async function relationalQuery(
|
|||
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
|
||||
> {
|
||||
const { parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
});
|
||||
|
||||
|
|
@ -34,6 +34,7 @@ async function relationalQuery(
|
|||
count(distinct session.country) as "countries",
|
||||
sum(case when website_event.event_type = 2 then 1 else 0 end) as "events"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
join session on website_event.session_id = session.session_id
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
|
@ -50,7 +51,7 @@ async function clickhouseQuery(
|
|||
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
|
||||
> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
});
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ async function clickhouseQuery(
|
|||
uniq(country) as "countries",
|
||||
sum(length(event_name)) as "events"
|
||||
from umami.website_event_stats_hourly "website_event"
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export async function getWebsiteSessions(
|
|||
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams: PageParams) {
|
||||
const { pagedRawQuery, parseFilters } = prisma;
|
||||
const { search } = pageParams;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
});
|
||||
|
||||
|
|
@ -24,7 +24,6 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
|
||||
return pagedRawQuery(
|
||||
`
|
||||
with sessions as (
|
||||
select
|
||||
session.session_id as "id",
|
||||
session.website_id as "websiteId",
|
||||
|
|
@ -43,6 +42,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
sum(case when website_event.event_type = 1 then 1 else 0 end) as "views",
|
||||
max(website_event.created_at) as "createdAt"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
join session on session.session_id = website_event.session_id
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
|
@ -68,8 +68,6 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
session.region,
|
||||
session.city
|
||||
order by max(website_event.created_at) desc
|
||||
limit 1000)
|
||||
select * from sessions
|
||||
`,
|
||||
{ ...params, search: `%${search}%` },
|
||||
pageParams,
|
||||
|
|
@ -78,12 +76,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
|
||||
const { pagedQuery, parseFilters, getDateStringSQL } = clickhouse;
|
||||
const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
|
||||
const { params, dateQuery, filterQuery, cohortQuery } = await parseFilters(websiteId, filters);
|
||||
const { search } = pageParams;
|
||||
|
||||
return pagedQuery(
|
||||
`
|
||||
with sessions as (
|
||||
select
|
||||
session_id as id,
|
||||
website_id as websiteId,
|
||||
|
|
@ -101,7 +98,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
uniq(visit_id) as visits,
|
||||
sumIf(views, event_type = 1) as views,
|
||||
lastAt as createdAt
|
||||
from website_event_stats_hourly
|
||||
from website_event_stats_hourly website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
|
|
@ -116,8 +114,6 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
}
|
||||
group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
|
||||
order by lastAt desc
|
||||
limit 1000)
|
||||
select * from sessions
|
||||
`,
|
||||
{ ...params, search },
|
||||
pageParams,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue