Compare commits

...

28 commits

Author SHA1 Message Date
Francis Cao
13ab84d50e Revert "add canonicalizeTimezone conversions"
Some checks failed
Create docker images (cloud) / Build, push, and deploy (push) Has been cancelled
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
This reverts commit a1d6204373.
2025-11-10 17:26:06 -08:00
Francis Cao
a1d6204373 add canonicalizeTimezone conversions
Co-authored-by: Om Mishra <contact@om-mishra.com>
2025-11-10 17:24:51 -08:00
Francis Cao
49e1582c28 implement generateTimeSeries for eventsChart 2025-11-10 15:36:43 -08:00
Francis Cao
64a6379c3c fix realtime logs for mobile
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-11-10 01:07:11 -08:00
Francis Cao
f3e246c64b fix hasdata queries, add hasData to website events, fix sessionactivity truncation, 2025-11-09 23:58:20 -08:00
Francis Cao
9230f3cb7b manually include basePath 2025-11-09 22:03:06 -08:00
Francis Cao
f30724629c Fix null and string return types from getWebsiteStats 2025-11-09 21:37:35 -08:00
Francis Cao
c44f6f8c9c Merge branch 'dev' of https://github.com/umami-software/umami into dev 2025-11-09 21:19:46 -08:00
Francis Cao
bf548c5aca Fix revenue bigInt but and case insensitive currency 2025-11-09 21:19:38 -08:00
Mike Cao
227201a73c
Merge pull request #3706 from metaloozee/3703
Some checks failed
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
fix: Redirect loop on auth failure
2025-11-08 11:32:02 -08:00
metaloozee
1879c161ee fix: Redirect loop on auth failure 2025-11-09 00:22:06 +05:30
Mike Cao
6ba9c1c40c New docker workflow.
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-11-07 22:41:10 -08:00
Mike Cao
de6515139e Fixed Docker permissions.
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-11-07 18:17:51 -08:00
Mike Cao
e3ca002d77 Fixed tags in build. 2025-11-07 14:35:05 -08:00
Mike Cao
8119dae3c3 Updated GH workflow. 2025-11-07 13:59:50 -08:00
Mike Cao
6ee93f7ac9 Updated README and cd.yml. 2025-11-07 12:21:17 -08:00
Mike Cao
3e9ca8761e Removed workflow script.
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-11-07 09:15:01 -08:00
Mike Cao
d2f512cae7 Don't publish .sig files. 2025-11-07 09:14:19 -08:00
Mike Cao
df3ca02e8b Always push latest for Docker. 2025-11-07 08:52:16 -08:00
Mike Cao
a90b788138 Updated cd script.
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-11-07 00:09:53 -08:00
Mike Cao
dd6556968c Updated image tag. 2025-11-06 23:58:12 -08:00
Mike Cao
04a05bbf26 Added workflow input. 2025-11-06 23:35:14 -08:00
Mike Cao
437c9603db Fixed build. 2025-11-06 22:58:26 -08:00
Mike Cao
03ed5349f4 Merge branch 'dev' 2025-11-06 22:50:30 -08:00
Mike Cao
4272bb4c4d Removed db types from docker build. 2025-11-06 22:48:34 -08:00
Mike Cao
6135ef9dd2 Fixed test. 2025-11-06 22:24:08 -08:00
Mike Cao
b5795a8b3f Fixed update notice.
Some checks failed
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
2025-11-06 16:16:53 -08:00
Mike Cao
98092004b6
Merge pull request #3683 from umami-software/dev
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
v3
2025-11-05 18:34:36 -08:00
20 changed files with 213 additions and 221 deletions

View file

@ -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 }}

View file

@ -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

View file

@ -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

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:postgresql-latest
docker pull docker.umami.is/umami-software/umami:latest
```
---

View file

@ -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:

View file

@ -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 <Loading placement="absolute" />;
}
if (error) {
if (process.env.cloudMode) {
window.location.href = '/login';
} else {
router.push('/login');
}
window.location.href = `${process.env.basePath || ''}/login`;
return null;
}

View file

@ -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 (
<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>
<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>
);
}

View file

@ -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}
</Text>
{row.hasData > 0 && <PropertiesButton websiteId={row.websiteId} eventId={row.id} />}
</Row>
);
}}
@ -72,3 +86,22 @@ 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,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 (
<Row alignItems="center" style={style} gap>
<Link href={updateParams({ session: row.sessionId })}>
<Avatar seed={row.sessionId} size={32} />
</Link>
<Row width="100px">{getTime(row)}</Row>
<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>
<IconLabel icon={getIcon(row)}>
<Text>{getDetail(row)}</Text>
<Text style={{ maxWidth: isPhone ? '400px' : null }} truncate>
{getDetail(row)}
</Text>
</IconLabel>
</Row>
);
@ -168,10 +176,22 @@ export function RealtimeLog({ data }: { data: any }) {
return (
<Column gap>
<Heading size="2">{formatMessage(labels.activity)}</Heading>
<Row alignItems="center" justifyContent="space-between">
<SearchField value={search} onSearch={setSearch} />
<FilterButtons items={buttons} value={filter} onChange={setFilter} />
</Row>
{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>
)}
<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 { 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 <PageBody isLoading={isLoading} error={error} />;
@ -48,7 +49,7 @@ export function RealtimePage({ websiteId }: { websiteId: string }) {
<Panel>
<RealtimeCountries data={countries} />
</Panel>
<Panel gridColumn="span 2" padding="0">
<Panel gridColumn={isMobile ? null : '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, 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 && <Heading size="1">{formatTimezoneDate(createdAt, 'PPPP')}</Heading>}
<Row alignItems="center" gap="6" height="40px">
<StatusLight color={`#${visitId?.substring(0, 6)}`}>
{formatTimezoneDate(createdAt, 'pp')}
<Text wrap="nowrap">{formatTimezoneDate(createdAt, 'pp')}</Text>
</StatusLight>
<Row alignItems="center" gap="2">
<Icon>{eventName ? <Lightning /> : <Eye />}</Icon>
<Text>
<Text wrap="nowrap">
{eventName
? formatMessage(labels.triggeredEvent)
: formatMessage(labels.viewedPage)}
</Text>
<Text weight="bold" style={{ maxWidth: '400px' }} truncate>
<Text weight="bold" style={{ maxWidth: isMobile ? '400px' : null }} 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');
router.push('/login');
window.location.href = `${process.env.basePath || ''}/login`;
}
removeClientAuthToken();

View file

@ -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<string>(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 (
<LoadingPanel isLoading={isLoading} error={error} minHeight="400px">
{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"
/>
)}

View file

@ -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
>
<Row alignItems="center">
<Text truncate={true} style={{ maxWidth: isMobile ? '200px' : '400px' }}>
<Text truncate={true} style={{ maxWidth: isPhone ? '200px' : '400px' }}>
{label}
</Text>
</Row>

View file

@ -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);
});

View file

@ -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<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,7 +45,11 @@ 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"
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"
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
sum(t.c) as "pageviews",
cast(coalesce(sum(t.c), 0) as bigint) as "pageviews",
count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
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"
from (
select
website_event.session_id,

View file

@ -41,6 +41,15 @@ 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
@ -48,17 +57,12 @@ async function relationalQuery(
${getDateSQL('revenue.created_at', unit, timezone)} t,
sum(revenue.revenue) y
from revenue
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}}
${joinQuery}
${cohortQuery}
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.currency like {{currency}}
and revenue.currency ilike {{currency}}
${filterQuery}
group by x, t
order by t
@ -72,19 +76,14 @@ async function relationalQuery(
session.country as name,
sum(revenue) value
from revenue
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}}
${joinQuery}
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 = {{currency}}
and revenue.currency ilike {{currency}}
${filterQuery}
group by session.country
`,
@ -98,23 +97,18 @@ async function relationalQuery(
count(distinct revenue.event_id) as count,
count(distinct revenue.session_id) as unique_count
from revenue
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}}
${joinQuery}
${cohortQuery}
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.currency = {{currency}}
and revenue.currency ilike {{currency}}
${filterQuery}
`,
queryParams,
).then(result => 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 };
}

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 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}}