Compare commits

..

No commits in common. "13ab84d50e7f8bfa4837ba8f7e48e97703b227dd" and "a6d4519a980dc301c7675ccf7ab69137b5469351" have entirely different histories.

20 changed files with 221 additions and 213 deletions

58
.github/workflows/cd-manual.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Create docker images (manual)
on:
workflow_dispatch:
inputs:
version:
type: string
description: Version
required: true
jobs:
build:
name: Build, push, and deploy
runs-on: ubuntu-latest
strategy:
matrix:
db-type: [postgresql]
steps:
- uses: actions/checkout@v3
- name: Extract version parts from input
id: extract_version
run: |
echo "version=$(echo ${{ github.event.inputs.version }})" >> $GITHUB_ENV
echo "major=$(echo ${{ github.event.inputs.version }} | cut -d. -f1)" >> $GITHUB_ENV
echo "minor=$(echo ${{ github.event.inputs.version }} | cut -d. -f2)" >> $GITHUB_ENV
- name: Generate tags
id: generate_tags
run: |
echo "tag_major=$(echo ${{ matrix.db-type }}-${{ env.major }})" >> $GITHUB_ENV
echo "tag_minor=$(echo ${{ matrix.db-type }}-${{ env.major }}.${{ env.minor }})" >> $GITHUB_ENV
echo "tag_patch=$(echo ${{ matrix.db-type }}-${{ env.version }})" >> $GITHUB_ENV
echo "tag_latest=$(echo ${{ matrix.db-type }}-latest)" >> $GITHUB_ENV
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
with:
image: umami
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: ghcr.io
multiPlatform: true
platform: linux/amd64,linux/arm64
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with:
image: umamisoftware/umami
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -5,11 +5,6 @@ on:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: 'Optional image version (e.g. 3.0.0, v3.0.0, or 3.0.0-beta.1)'
required: false
default: ''
jobs:
build:
@ -18,20 +13,22 @@ jobs:
permissions:
contents: read
packages: write
id-token: write
strategy:
matrix:
db-type: [postgresql]
steps:
- uses: actions/checkout@v5
# Install cosign (for image signing)
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log into Docker Hub
if: github.repository == 'umami-software/umami'
uses: docker/login-action@v3
@ -40,61 +37,44 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Compute version tags
id: compute
run: |
INPUT="${{ github.event.inputs.version }}"
REF_TYPE="${{ github.ref_type }}"
REF_NAME="${{ github.ref_name }}"
- name: Log into GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Determine version source
if [[ -n "$INPUT" ]]; then
VERSION="${INPUT#v}"
elif [[ "$REF_TYPE" == "tag" ]]; then
VERSION="${REF_NAME#v}"
else
VERSION=""
fi
TAGS=""
if [[ -n "$VERSION" ]]; then
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
if [[ "$VERSION" == *-* ]]; then
# prerelease: only version tag
TAGS="$VERSION"
else
# stable release: version + hierarchy + latest
TAGS="$VERSION,${MAJOR}.${MINOR},${MAJOR},postgresql-latest,latest"
fi
else
# Non-tag build (e.g. from main branch)
TAGS="${REF_NAME}"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
echo "Computed tags: $TAGS"
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
umamisoftware/umami,enable=${{ github.repository == 'umami-software/umami' }}
ghcr.io/${{ github.repository }}
flavor: |
latest=auto
prefix=${{ matrix.db-type }}-
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push Docker image
run: |
TAGS="${{ steps.compute.outputs.tags }}"
id: build-and-push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
build-args: DATABASE_TYPE=${{ matrix.db-type }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Set image targets conditionally
if [[ "${{ github.repository }}" == "umami-software/umami" ]]; then
IMAGES=("umamisoftware/umami" "ghcr.io/${{ github.repository }}")
else
IMAGES=("ghcr.io/${{ github.repository }}")
fi
for IMAGE in "${IMAGES[@]}"; do
echo "Building and pushing $IMAGE with tags: $TAGS"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
$(echo "$TAGS" | tr ',' '\n' | sed "s|^|--tag ${IMAGE}:|") \
--cache-from type=gha \
--cache-to type=gha,mode=max \
.
done
# Sign the published image digest
- name: Sign the published Docker image
env:
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: echo "${TAGS}" | xargs -I {} cosign sign --yes "{}@${DIGEST}"

View file

@ -0,0 +1,22 @@
name: Delete untagged GHCR images
on:
workflow_dispatch: # Run manually from the Actions tab
jobs:
cleanup:
name: Delete all untagged images
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Delete untagged GHCR images
uses: actions/delete-package-versions@v5
with:
package-name: "umami" # 👈 change if your GHCR package name differs
package-type: "container"
delete-only-untagged-versions: true
min-versions-to-keep: 0

View file

@ -89,7 +89,7 @@ docker compose up -d
Alternatively, to pull just the Umami Docker image with PostgreSQL support:
```bash
docker pull docker.umami.is/umami-software/umami:latest
docker pull docker.umami.is/umami-software/umami:postgresql-latest
```
---

View file

@ -1,7 +1,7 @@
---
services:
umami:
image: ghcr.io/umami-software/umami:latest
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- "3000:3000"
environment:

View file

@ -9,14 +9,18 @@ import { MobileNav } from '@/app/(main)/MobileNav';
export function App({ children }) {
const { user, isLoading, error } = useLoginQuery();
const config = useConfig();
const { pathname } = useNavigation();
const { pathname, router } = useNavigation();
if (isLoading || !config) {
return <Loading placement="absolute" />;
}
if (error) {
window.location.href = `${process.env.basePath || ''}/login`;
if (process.env.cloudMode) {
window.location.href = '/login';
} else {
router.push('/login');
}
return null;
}

View file

@ -1,5 +1,5 @@
import { useEffect, useCallback, useState } from 'react';
import { Button, AlertBanner, Column, Row } from '@umami/react-zen';
import { Button, AlertBanner, Flexbox } from '@umami/react-zen';
import { setItem } from '@/lib/storage';
import { useVersion, checkVersion } from '@/store/version';
import { REPO_URL, VERSION_CHECK } from '@/lib/constants';
@ -47,15 +47,13 @@ export function UpdateNotice({ user, config }) {
}
return (
<Column justifyContent="center" alignItems="center" position="fixed" top="10px" width="100%">
<Row width="600px">
<AlertBanner title={formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}>
<Button variant="primary" onPress={handleViewClick}>
{formatMessage(labels.viewDetails)}
</Button>
<Button onPress={handleDismissClick}>{formatMessage(labels.dismiss)}</Button>
</AlertBanner>
</Row>
</Column>
<Flexbox justifyContent="space-between" alignItems="center">
<AlertBanner title={formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}>
<Button variant="primary" onPress={handleViewClick}>
{formatMessage(labels.viewDetails)}
</Button>
<Button onPress={handleDismissClick}>{formatMessage(labels.dismiss)}</Button>
</AlertBanner>
</Flexbox>
);
}

View file

@ -1,24 +1,11 @@
import {
DataTable,
DataColumn,
Row,
Text,
DataTableProps,
IconLabel,
Button,
Dialog,
DialogTrigger,
Icon,
Popover,
} from '@umami/react-zen';
import { DataTable, DataColumn, Row, Text, DataTableProps, IconLabel } from '@umami/react-zen';
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link';
import { Eye, FileText } from '@/components/icons';
import { Eye } from '@/components/icons';
import { Lightning } from '@/components/svg';
import { DateDistance } from '@/components/common/DateDistance';
import { TypeIcon } from '@/components/common/TypeIcon';
import { EventData } from '@/components/metrics/EventData';
export function EventsTable(props: DataTableProps) {
const { formatMessage, labels } = useMessages();
@ -45,7 +32,6 @@ export function EventsTable(props: DataTableProps) {
>
{row.eventName || row.urlPath}
</Text>
{row.hasData > 0 && <PropertiesButton websiteId={row.websiteId} eventId={row.id} />}
</Row>
);
}}
@ -86,22 +72,3 @@ export function EventsTable(props: DataTableProps) {
</DataTable>
);
}
const PropertiesButton = props => {
return (
<DialogTrigger>
<Button variant="quiet">
<Row alignItems="center" gap>
<Icon>
<FileText />
</Icon>
</Row>
</Button>
<Popover placement="right">
<Dialog>
<EventData {...props} />
</Dialog>
</Popover>
</DialogTrigger>
);
};

View file

@ -9,7 +9,6 @@ import {
useCountryNames,
useLocale,
useMessages,
useMobile,
useNavigation,
useTimezone,
useWebsite,
@ -41,7 +40,6 @@ export function RealtimeLog({ data }: { data: any }) {
const { countryNames } = useCountryNames(locale);
const [filter, setFilter] = useState(TYPE_ALL);
const { updateParams } = useNavigation();
const { isPhone } = useMobile();
const buttons = [
{
@ -125,18 +123,12 @@ export function RealtimeLog({ data }: { data: any }) {
const row = logs[index];
return (
<Row alignItems="center" style={style} gap>
<Row minWidth="30px">
<Link href={updateParams({ session: row.sessionId })}>
<Avatar seed={row.sessionId} size={32} />
</Link>
</Row>
<Row minWidth="100px">
<Text wrap="nowrap">{getTime(row)}</Text>
</Row>
<Link href={updateParams({ session: row.sessionId })}>
<Avatar seed={row.sessionId} size={32} />
</Link>
<Row width="100px">{getTime(row)}</Row>
<IconLabel icon={getIcon(row)}>
<Text style={{ maxWidth: isPhone ? '400px' : null }} truncate>
{getDetail(row)}
</Text>
<Text>{getDetail(row)}</Text>
</IconLabel>
</Row>
);
@ -176,22 +168,10 @@ export function RealtimeLog({ data }: { data: any }) {
return (
<Column gap>
<Heading size="2">{formatMessage(labels.activity)}</Heading>
{isPhone ? (
<>
<Row>
<SearchField value={search} onSearch={setSearch} />
</Row>
<Row>
<FilterButtons items={buttons} value={filter} onChange={setFilter} />
</Row>
</>
) : (
<Row alignItems="center" justifyContent="space-between">
<SearchField value={search} onSearch={setSearch} />
<FilterButtons items={buttons} value={filter} onChange={setFilter} />
</Row>
)}
<Row alignItems="center" justifyContent="space-between">
<SearchField value={search} onSearch={setSearch} />
<FilterButtons items={buttons} value={filter} onChange={setFilter} />
</Row>
<Column>
{logs?.length === 0 && <Empty />}
{logs?.length > 0 && (

View file

@ -6,7 +6,7 @@ import { PageBody } from '@/components/common/PageBody';
import { Panel } from '@/components/common/Panel';
import { RealtimeChart } from '@/components/metrics/RealtimeChart';
import { WorldMap } from '@/components/metrics/WorldMap';
import { useMobile, useRealtimeQuery } from '@/components/hooks';
import { useRealtimeQuery } from '@/components/hooks';
import { RealtimeLog } from './RealtimeLog';
import { RealtimeHeader } from './RealtimeHeader';
import { RealtimePaths } from './RealtimePaths';
@ -16,7 +16,6 @@ import { percentFilter } from '@/lib/filters';
export function RealtimePage({ websiteId }: { websiteId: string }) {
const { data, isLoading, error } = useRealtimeQuery(websiteId);
const { isMobile } = useMobile();
if (isLoading || error) {
return <PageBody isLoading={isLoading} error={error} />;
@ -49,7 +48,7 @@ export function RealtimePage({ websiteId }: { websiteId: string }) {
<Panel>
<RealtimeCountries data={countries} />
</Panel>
<Panel gridColumn={isMobile ? null : 'span 2'} padding="0">
<Panel gridColumn="span 2" padding="0">
<WorldMap data={countries} />
</Panel>
</GridRow>

View file

@ -14,7 +14,7 @@ import {
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Eye, FileText } from '@/components/icons';
import { Lightning } from '@/components/svg';
import { useMessages, useMobile, useSessionActivityQuery, useTimezone } from '@/components/hooks';
import { useMessages, useSessionActivityQuery, useTimezone } from '@/components/hooks';
import { EventData } from '@/components/metrics/EventData';
export function SessionActivity({
@ -36,7 +36,6 @@ export function SessionActivity({
startDate,
endDate,
);
const { isMobile } = useMobile();
let lastDay = null;
return (
@ -51,16 +50,16 @@ export function SessionActivity({
{showHeader && <Heading size="1">{formatTimezoneDate(createdAt, 'PPPP')}</Heading>}
<Row alignItems="center" gap="6" height="40px">
<StatusLight color={`#${visitId?.substring(0, 6)}`}>
<Text wrap="nowrap">{formatTimezoneDate(createdAt, 'pp')}</Text>
{formatTimezoneDate(createdAt, 'pp')}
</StatusLight>
<Row alignItems="center" gap="2">
<Icon>{eventName ? <Lightning /> : <Eye />}</Icon>
<Text wrap="nowrap">
<Text>
{eventName
? formatMessage(labels.triggeredEvent)
: formatMessage(labels.viewedPage)}
</Text>
<Text weight="bold" style={{ maxWidth: isMobile ? '400px' : null }} truncate>
<Text weight="bold" style={{ maxWidth: '400px' }} truncate>
{eventName || urlPath}
</Text>
{hasData > 0 && <PropertiesButton websiteId={websiteId} eventId={eventId} />}

View file

@ -13,7 +13,7 @@ export function LogoutPage() {
async function logout() {
await post('/auth/logout');
window.location.href = `${process.env.basePath || ''}/login`;
router.push('/login');
}
removeClientAuthToken();

View file

@ -1,11 +1,10 @@
import { useMemo, useState, useEffect } from 'react';
import { colord } from 'colord';
import { BarChart, BarChartProps } from '@/components/charts/BarChart';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useDateRange, useLocale, useWebsiteEventsSeriesQuery } from '@/components/hooks';
import { renderDateLabels } from '@/lib/charts';
import { CHART_COLORS } from '@/lib/constants';
import { generateTimeSeries } from '@/lib/date';
import { colord } from 'colord';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { LoadingPanel } from '@/components/common/LoadingPanel';
export interface EventsChartProps extends BarChartProps {
websiteId: string;
@ -16,7 +15,7 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
const {
dateRange: { startDate, endDate, unit },
} = useDateRange();
const { locale, dateLocale } = useLocale();
const { locale } = useLocale();
const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId);
const [label, setLabel] = useState<string>(focusLabel);
@ -38,7 +37,7 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
return {
label: key,
data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale),
data: map[key],
lineTension: 0,
backgroundColor: color.alpha(0.6).toRgbString(),
borderColor: color.alpha(0.7).toRgbString(),
@ -55,8 +54,6 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
}
}, [focusLabel]);
const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
return (
<LoadingPanel isLoading={isLoading} error={error} minHeight="400px">
{chartData && (
@ -66,7 +63,7 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
maxDate={endDate}
unit={unit}
stacked={true}
renderXLabel={renderXLabel}
renderXLabel={renderDateLabels(unit, locale)}
height="400px"
/>
)}

View file

@ -57,7 +57,7 @@ export function ListTable({
showPercentage={showPercentage}
change={renderChange ? renderChange(row, index) : null}
currency={currency}
isPhone={isPhone}
isMobile={isPhone}
/>
);
};
@ -101,7 +101,7 @@ const AnimatedRow = ({
animate,
showPercentage = true,
currency,
isPhone,
isMobile,
}) => {
const props = useSpring({
width: percent,
@ -120,7 +120,7 @@ const AnimatedRow = ({
gap
>
<Row alignItems="center">
<Text truncate={true} style={{ maxWidth: isPhone ? '200px' : '400px' }}>
<Text truncate={true} style={{ maxWidth: isMobile ? '200px' : '400px' }}>
{label}
</Text>
</Row>

View file

@ -1,4 +1,4 @@
import { getIpAddress } from '../ip';
import * as detect from '../detect';
const IP = '127.0.0.1';
const BAD_IP = '127.127.127.127';
@ -6,23 +6,23 @@ const BAD_IP = '127.127.127.127';
test('getIpAddress: Custom header', () => {
process.env.CLIENT_IP_HEADER = 'x-custom-ip-header';
expect(getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP);
expect(detect.getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP);
});
test('getIpAddress: CloudFlare header', () => {
expect(getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP);
expect(detect.getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP);
});
test('getIpAddress: Standard header', () => {
expect(getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP);
expect(detect.getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP);
});
test('getIpAddress: CloudFlare header is lower priority than standard header', () => {
expect(getIpAddress(new Headers({ 'cf-connecting-ip': BAD_IP, 'x-forwarded-for': IP }))).toEqual(
IP,
);
expect(
detect.getIpAddress(new Headers({ 'cf-connecting-ip': BAD_IP, 'x-forwarded-for': IP })),
).toEqual(IP);
});
test('getIpAddress: No header', () => {
expect(getIpAddress(new Headers())).toEqual(null);
expect(detect.getIpAddress(new Headers())).toEqual(null);
});

View file

@ -19,20 +19,20 @@ async function relationalQuery(websiteId: string, eventId: string) {
return rawQuery(
`
select event_data.website_id as "websiteId",
event_data.website_event_id as "eventId",
website_event.event_name as "eventName",
event_data.data_key as "dataKey",
event_data.string_value as "stringValue",
event_data.number_value as "numberValue",
event_data.date_value as "dateValue",
event_data.data_type as "dataType",
event_data.created_at as "createdAt"
select website_id as "websiteId",
session_id as "sessionId",
event_id as "eventId",
url_path as "urlPath",
event_name as "eventName",
data_key as "dataKey",
string_value as "stringValue",
number_value as "numberValue",
date_value as "dateValue",
data_type as "dataType",
created_at as "createdAt"
from event_data
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
where event_data.website_id = {{websiteId::uuid}}
and event_data.website_event_id = {{eventId::uuid}}
website_id = {{websiteId::uuid}}
event_id = {{eventId::uuid}}
`,
{ websiteId, eventId },
FUNCTION_NAME,
@ -45,7 +45,9 @@ async function clickhouseQuery(websiteId: string, eventId: string): Promise<Even
return rawQuery(
`
select website_id as websiteId,
session_id as sessionId,
event_id as eventId,
url_path as urlPath,
event_name as eventName,
data_key as dataKey,
string_value as stringValue,

View file

@ -45,11 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
browser as browser,
page_title as "pageTitle",
website_event.event_type as "eventType",
website_event.event_name as "eventName",
event_id IN (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}) AS "hasData"
website_event.event_name as "eventName"
from website_event
${cohortQuery}
join session on session.session_id = website_event.session_id

View file

@ -36,11 +36,11 @@ async function relationalQuery(
return rawQuery(
`
select
cast(coalesce(sum(t.c), 0) as bigint) as "pageviews",
sum(t.c) as "pageviews",
count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits",
coalesce(sum(case when t.c = 1 then 1 else 0 end), 0) as "bounces",
cast(coalesce(sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}), 0) as bigint) as "totaltime"
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
website_event.session_id,

View file

@ -41,15 +41,6 @@ async function relationalQuery(
currency,
});
const joinQuery = filterQuery
? `join website_event
on website_event.website_id = revenue.website_id
and website_event.session_id = revenue.session_id
and website_event.event_id = revenue.event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}`
: '';
const chart = await rawQuery(
`
select
@ -57,12 +48,17 @@ async function relationalQuery(
${getDateSQL('revenue.created_at', unit, timezone)} t,
sum(revenue.revenue) y
from revenue
${joinQuery}
join website_event
on website_event.website_id = revenue.website_id
and website_event.session_id = revenue.session_id
and website_event.event_id = revenue.event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.currency ilike {{currency}}
and revenue.currency like {{currency}}
${filterQuery}
group by x, t
order by t
@ -76,14 +72,19 @@ async function relationalQuery(
session.country as name,
sum(revenue) value
from revenue
${joinQuery}
join website_event
on website_event.website_id = revenue.website_id
and website_event.session_id = revenue.session_id
and website_event.event_id = revenue.event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
join session
on session.website_id = revenue.website_id
and session.session_id = revenue.session_id
${cohortQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.currency ilike {{currency}}
and revenue.currency = {{currency}}
${filterQuery}
group by session.country
`,
@ -97,18 +98,23 @@ async function relationalQuery(
count(distinct revenue.event_id) as count,
count(distinct revenue.session_id) as unique_count
from revenue
${joinQuery}
join website_event
on website_event.website_id = revenue.website_id
and website_event.session_id = revenue.session_id
and website_event.event_id = revenue.event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.currency ilike {{currency}}
and revenue.currency = {{currency}}
${filterQuery}
`,
queryParams,
).then(result => result?.[0]);
total.average = total.count > 0 ? Number(total.sum) / Number(total.count) : 0;
total.average = total.count > 0 ? total.sum / total.count : 0;
return { chart, country, total };
}

View file

@ -29,10 +29,10 @@ async function relationalQuery(websiteId: string, sessionId: string, filters: Qu
event_type as "eventType",
event_name as "eventName",
visit_id as "visitId",
event_id IN (select website_event_id
event_id IN (select event_id
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}) AS "hasData"
and session_id = {{sessionId::uuid}}) AS "hasData"
from website_event
where website_id = {{websiteId::uuid}}
and session_id = {{sessionId::uuid}}