diff --git a/.github/workflows/cd-manual.yml b/.github/workflows/cd-manual.yml
deleted file mode 100644
index df6aa6289..000000000
--- a/.github/workflows/cd-manual.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-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 }}
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index a02e9900c..a9509bce0 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -5,6 +5,11 @@ 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:
@@ -13,22 +18,20 @@ 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
@@ -37,44 +40,61 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- - name: Log into GHCR
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Compute version tags
+ id: compute
+ run: |
+ INPUT="${{ github.event.inputs.version }}"
+ REF_TYPE="${{ github.ref_type }}"
+ REF_NAME="${{ github.ref_name }}"
- - 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}}
+ # 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: Build and push Docker image
- 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
+ run: |
+ TAGS="${{ steps.compute.outputs.tags }}"
- # 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}"
+ # 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
diff --git a/.github/workflows/delete-untagged-images.yml b/.github/workflows/delete-untagged-images.yml
deleted file mode 100644
index a23a1bd27..000000000
--- a/.github/workflows/delete-untagged-images.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-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
diff --git a/README.md b/README.md
index 6d166d8c8..d3791e269 100644
--- a/README.md
+++ b/README.md
@@ -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:postgresql-latest
+docker pull docker.umami.is/umami-software/umami:latest
```
---
diff --git a/docker-compose.yml b/docker-compose.yml
index 7b51db66c..8c8a47a6e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,7 @@
---
services:
umami:
- image: ghcr.io/umami-software/umami:postgresql-latest
+ image: ghcr.io/umami-software/umami:latest
ports:
- "3000:3000"
environment:
diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx
index 32218d115..ec08838d1 100644
--- a/src/app/(main)/App.tsx
+++ b/src/app/(main)/App.tsx
@@ -9,18 +9,14 @@ import { MobileNav } from '@/app/(main)/MobileNav';
export function App({ children }) {
const { user, isLoading, error } = useLoginQuery();
const config = useConfig();
- const { pathname, router } = useNavigation();
+ const { pathname } = useNavigation();
if (isLoading || !config) {
return ;
}
if (error) {
- if (process.env.cloudMode) {
- window.location.href = '/login';
- } else {
- router.push('/login');
- }
+ window.location.href = `${process.env.basePath || ''}/login`;
return null;
}
diff --git a/src/app/(main)/UpdateNotice.tsx b/src/app/(main)/UpdateNotice.tsx
index 357287912..81e2ca3af 100644
--- a/src/app/(main)/UpdateNotice.tsx
+++ b/src/app/(main)/UpdateNotice.tsx
@@ -1,5 +1,5 @@
import { useEffect, useCallback, useState } from 'react';
-import { Button, AlertBanner, Flexbox } from '@umami/react-zen';
+import { Button, AlertBanner, Column, Row } from '@umami/react-zen';
import { setItem } from '@/lib/storage';
import { useVersion, checkVersion } from '@/store/version';
import { REPO_URL, VERSION_CHECK } from '@/lib/constants';
@@ -47,13 +47,15 @@ export function UpdateNotice({ user, config }) {
}
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
index e9e3e6a0c..ea0edde1b 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
@@ -1,11 +1,24 @@
-import { DataTable, DataColumn, Row, Text, DataTableProps, IconLabel } from '@umami/react-zen';
+import {
+ DataTable,
+ DataColumn,
+ Row,
+ Text,
+ DataTableProps,
+ IconLabel,
+ Button,
+ Dialog,
+ DialogTrigger,
+ Icon,
+ Popover,
+} from '@umami/react-zen';
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link';
-import { Eye } from '@/components/icons';
+import { Eye, FileText } 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();
@@ -32,6 +45,7 @@ export function EventsTable(props: DataTableProps) {
>
{row.eventName || row.urlPath}
+ {row.hasData > 0 && }
);
}}
@@ -72,3 +86,22 @@ export function EventsTable(props: DataTableProps) {
);
}
+
+const PropertiesButton = props => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
index 9ae19bf89..3dec340f2 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
@@ -9,6 +9,7 @@ import {
useCountryNames,
useLocale,
useMessages,
+ useMobile,
useNavigation,
useTimezone,
useWebsite,
@@ -40,6 +41,7 @@ export function RealtimeLog({ data }: { data: any }) {
const { countryNames } = useCountryNames(locale);
const [filter, setFilter] = useState(TYPE_ALL);
const { updateParams } = useNavigation();
+ const { isPhone } = useMobile();
const buttons = [
{
@@ -123,12 +125,18 @@ export function RealtimeLog({ data }: { data: any }) {
const row = logs[index];
return (
-
-
-
- {getTime(row)}
+
+
+
+
+
+
+ {getTime(row)}
+
- {getDetail(row)}
+
+ {getDetail(row)}
+
);
@@ -168,10 +176,22 @@ export function RealtimeLog({ data }: { data: any }) {
return (
{formatMessage(labels.activity)}
-
-
-
-
+ {isPhone ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
+
+ )}
+
{logs?.length === 0 && }
{logs?.length > 0 && (
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx
index 7f9ab6085..0f9fa358b 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx
@@ -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 { useRealtimeQuery } from '@/components/hooks';
+import { useMobile, useRealtimeQuery } from '@/components/hooks';
import { RealtimeLog } from './RealtimeLog';
import { RealtimeHeader } from './RealtimeHeader';
import { RealtimePaths } from './RealtimePaths';
@@ -16,6 +16,7 @@ import { percentFilter } from '@/lib/filters';
export function RealtimePage({ websiteId }: { websiteId: string }) {
const { data, isLoading, error } = useRealtimeQuery(websiteId);
+ const { isMobile } = useMobile();
if (isLoading || error) {
return ;
@@ -48,7 +49,7 @@ export function RealtimePage({ websiteId }: { websiteId: string }) {
-
+
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
index b9f34e485..7bcf1b760 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
@@ -14,7 +14,7 @@ import {
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Eye, FileText } from '@/components/icons';
import { Lightning } from '@/components/svg';
-import { useMessages, useSessionActivityQuery, useTimezone } from '@/components/hooks';
+import { useMessages, useMobile, useSessionActivityQuery, useTimezone } from '@/components/hooks';
import { EventData } from '@/components/metrics/EventData';
export function SessionActivity({
@@ -36,6 +36,7 @@ export function SessionActivity({
startDate,
endDate,
);
+ const { isMobile } = useMobile();
let lastDay = null;
return (
@@ -50,16 +51,16 @@ export function SessionActivity({
{showHeader && {formatTimezoneDate(createdAt, 'PPPP')}}
- {formatTimezoneDate(createdAt, 'pp')}
+ {formatTimezoneDate(createdAt, 'pp')}
{eventName ? : }
-
+
{eventName
? formatMessage(labels.triggeredEvent)
: formatMessage(labels.viewedPage)}
-
+
{eventName || urlPath}
{hasData > 0 && }
diff --git a/src/app/logout/LogoutPage.tsx b/src/app/logout/LogoutPage.tsx
index 909f35de3..d66d62a98 100644
--- a/src/app/logout/LogoutPage.tsx
+++ b/src/app/logout/LogoutPage.tsx
@@ -13,7 +13,7 @@ export function LogoutPage() {
async function logout() {
await post('/auth/logout');
- router.push('/login');
+ window.location.href = `${process.env.basePath || ''}/login`;
}
removeClientAuthToken();
diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx
index 7301faf44..246772b3e 100644
--- a/src/components/metrics/EventsChart.tsx
+++ b/src/components/metrics/EventsChart.tsx
@@ -1,10 +1,11 @@
-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 { LoadingPanel } from '@/components/common/LoadingPanel';
+import { generateTimeSeries } from '@/lib/date';
+import { colord } from 'colord';
+import { useCallback, useEffect, useMemo, useState } from 'react';
export interface EventsChartProps extends BarChartProps {
websiteId: string;
@@ -15,7 +16,7 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
const {
dateRange: { startDate, endDate, unit },
} = useDateRange();
- const { locale } = useLocale();
+ const { locale, dateLocale } = useLocale();
const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId);
const [label, setLabel] = useState(focusLabel);
@@ -37,7 +38,7 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
return {
label: key,
- data: map[key],
+ data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale),
lineTension: 0,
backgroundColor: color.alpha(0.6).toRgbString(),
borderColor: color.alpha(0.7).toRgbString(),
@@ -54,6 +55,8 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
}
}, [focusLabel]);
+ const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
+
return (
{chartData && (
@@ -63,7 +66,7 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
maxDate={endDate}
unit={unit}
stacked={true}
- renderXLabel={renderDateLabels(unit, locale)}
+ renderXLabel={renderXLabel}
height="400px"
/>
)}
diff --git a/src/components/metrics/ListTable.tsx b/src/components/metrics/ListTable.tsx
index 303556b01..e76e01745 100644
--- a/src/components/metrics/ListTable.tsx
+++ b/src/components/metrics/ListTable.tsx
@@ -57,7 +57,7 @@ export function ListTable({
showPercentage={showPercentage}
change={renderChange ? renderChange(row, index) : null}
currency={currency}
- isMobile={isPhone}
+ isPhone={isPhone}
/>
);
};
@@ -101,7 +101,7 @@ const AnimatedRow = ({
animate,
showPercentage = true,
currency,
- isMobile,
+ isPhone,
}) => {
const props = useSpring({
width: percent,
@@ -120,7 +120,7 @@ const AnimatedRow = ({
gap
>
-
+
{label}
diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts
index fcf706af1..f02ac8392 100644
--- a/src/lib/__tests__/detect.test.ts
+++ b/src/lib/__tests__/detect.test.ts
@@ -1,4 +1,4 @@
-import * as detect from '../detect';
+import { getIpAddress } from '../ip';
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(detect.getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP);
+ expect(getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP);
});
test('getIpAddress: CloudFlare header', () => {
- expect(detect.getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP);
+ expect(getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP);
});
test('getIpAddress: Standard header', () => {
- expect(detect.getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP);
+ expect(getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP);
});
test('getIpAddress: CloudFlare header is lower priority than standard header', () => {
- expect(
- detect.getIpAddress(new Headers({ 'cf-connecting-ip': BAD_IP, 'x-forwarded-for': IP })),
- ).toEqual(IP);
+ expect(getIpAddress(new Headers({ 'cf-connecting-ip': BAD_IP, 'x-forwarded-for': IP }))).toEqual(
+ IP,
+ );
});
test('getIpAddress: No header', () => {
- expect(detect.getIpAddress(new Headers())).toEqual(null);
+ expect(getIpAddress(new Headers())).toEqual(null);
});
diff --git a/src/queries/sql/events/getEventData.ts b/src/queries/sql/events/getEventData.ts
index 42dc2040b..269258a8d 100644
--- a/src/queries/sql/events/getEventData.ts
+++ b/src/queries/sql/events/getEventData.ts
@@ -19,20 +19,20 @@ async function relationalQuery(websiteId: string, eventId: string) {
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",
- number_value as "numberValue",
- date_value as "dateValue",
- data_type as "dataType",
- created_at as "createdAt"
+ 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"
from event_data
- website_id = {{websiteId::uuid}}
- event_id = {{eventId::uuid}}
+ 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}}
`,
{ websiteId, eventId },
FUNCTION_NAME,
@@ -45,9 +45,7 @@ async function clickhouseQuery(websiteId: string, eventId: string): Promise result?.[0]);
- total.average = total.count > 0 ? total.sum / total.count : 0;
+ total.average = total.count > 0 ? Number(total.sum) / Number(total.count) : 0;
return { chart, country, total };
}
diff --git a/src/queries/sql/sessions/getSessionActivity.ts b/src/queries/sql/sessions/getSessionActivity.ts
index 360db530a..3dd4fa9d8 100644
--- a/src/queries/sql/sessions/getSessionActivity.ts
+++ b/src/queries/sql/sessions/getSessionActivity.ts
@@ -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 event_id
+ event_id IN (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
- and session_id = {{sessionId::uuid}}) AS "hasData"
+ and created_at between {{startDate}} and {{endDate}}) AS "hasData"
from website_event
where website_id = {{websiteId::uuid}}
and session_id = {{sessionId::uuid}}