mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 06:37:18 +01:00
Merge branch 'dev' of https://github.com/umami-software/umami into dev
This commit is contained in:
commit
1483241494
93 changed files with 3147 additions and 1296 deletions
|
|
@ -21,7 +21,11 @@ export function LinksTable(props: DataTableProps) {
|
|||
<DataColumn id="slug" label={formatMessage(labels.link)}>
|
||||
{({ slug }: any) => {
|
||||
const url = getSlugUrl(slug);
|
||||
return <ExternalLink href={url}>{url}</ExternalLink>;
|
||||
return (
|
||||
<ExternalLink href={url} prefetch={false}>
|
||||
{url}
|
||||
</ExternalLink>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="url" label={formatMessage(labels.destinationUrl)}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Icon, Text } from '@umami/react-zen';
|
||||
import { IconLabel } from '@umami/react-zen';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useLink, useMessages, useSlug } from '@/components/hooks';
|
||||
|
|
@ -10,12 +10,9 @@ export function LinkHeader() {
|
|||
const link = useLink();
|
||||
|
||||
return (
|
||||
<PageHeader title={link.name} description={link.url} icon={<Link />} marginBottom="3">
|
||||
<LinkButton href={getSlugUrl(link.slug)} target="_blank">
|
||||
<Icon>
|
||||
<ExternalLink />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.view)}</Text>
|
||||
<PageHeader title={link.name} description={link.url} icon={<Link />}>
|
||||
<LinkButton href={getSlugUrl(link.slug)} target="_blank" prefetch={false} asAnchor>
|
||||
<IconLabel icon={<ExternalLink />} label={formatMessage(labels.view)} />
|
||||
</LinkButton>
|
||||
</PageHeader>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ export function PixelsTable(props: DataTableProps) {
|
|||
<DataColumn id="url" label="URL">
|
||||
{({ slug }: any) => {
|
||||
const url = getSlugUrl(slug);
|
||||
return <ExternalLink href={url}>{url}</ExternalLink>;
|
||||
return (
|
||||
<ExternalLink href={url} prefetch={false}>
|
||||
{url}
|
||||
</ExternalLink>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Icon, Text } from '@umami/react-zen';
|
||||
import { IconLabel } from '@umami/react-zen';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useMessages, usePixel, useSlug } from '@/components/hooks';
|
||||
|
|
@ -10,12 +10,9 @@ export function PixelHeader() {
|
|||
const pixel = usePixel();
|
||||
|
||||
return (
|
||||
<PageHeader title={pixel.name} icon={<Grid2x2 />} marginBottom="3">
|
||||
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank" prefetch={false}>
|
||||
<Icon>
|
||||
<ExternalLink />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.view)}</Text>
|
||||
<PageHeader title={pixel.name} icon={<Grid2x2 />}>
|
||||
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank" prefetch={false} asAnchor>
|
||||
<IconLabel icon={<ExternalLink />} label={formatMessage(labels.view)} />
|
||||
</LinkButton>
|
||||
</PageHeader>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import Link from 'next/link';
|
||||
import { DataGrid } from '@/components/common/DataGrid';
|
||||
import { useLoginQuery, useNavigation, useUserWebsitesQuery } from '@/components/hooks';
|
||||
import { Favicon } from '@/index';
|
||||
import { Icon, Row } from '@umami/react-zen';
|
||||
import { WebsitesTable } from './WebsitesTable';
|
||||
|
||||
export function WebsitesDataTable({
|
||||
|
|
@ -21,7 +23,12 @@ export function WebsitesDataTable({
|
|||
const { renderUrl } = useNavigation();
|
||||
|
||||
const renderLink = (row: any) => (
|
||||
<Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>
|
||||
<Row alignItems="center" gap="3">
|
||||
<Icon size="md" color="muted">
|
||||
<Favicon domain={row.domain} />
|
||||
</Icon>
|
||||
<Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -13,12 +13,18 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
|||
const { renderUrl, pathname } = useNavigation();
|
||||
const isSettings = pathname.endsWith('/settings');
|
||||
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
if (isSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} marginBottom="3">
|
||||
<PageHeader
|
||||
title={website.name}
|
||||
icon={<Favicon domain={website.domain} />}
|
||||
titleHref={renderUrl(`/websites/${website.id}`, false)}
|
||||
>
|
||||
<Row alignItems="center" gap="6" wrap="wrap">
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
|
||||
|
|
@ -29,7 +35,7 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
|||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
<Text>Edit</Text>
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</LinkButton>
|
||||
</Row>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { checkPassword } from '@/lib/password';
|
|||
import redis from '@/lib/redis';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { getUserByUsername } from '@/queries/prisma';
|
||||
import { getAllUserTeams, getUserByUsername } from '@/queries/prisma';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
|
|
@ -39,8 +39,10 @@ export async function POST(request: Request) {
|
|||
token = createSecureToken({ userId: user.id, role }, secret());
|
||||
}
|
||||
|
||||
const teams = await getAllUserTeams(id);
|
||||
|
||||
return json({
|
||||
token,
|
||||
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin },
|
||||
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin, teams },
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export async function POST(request: Request) {
|
|||
const errors = [];
|
||||
|
||||
let index = 0;
|
||||
let cache = null;
|
||||
for (const data of body) {
|
||||
// Recreate a fresh Request since `new Request(request)` will have the following error:
|
||||
// > Cannot read private member #state from an object whose class did not declare it
|
||||
|
|
@ -33,9 +34,12 @@ export async function POST(request: Request) {
|
|||
});
|
||||
|
||||
const response = await send.POST(newRequest);
|
||||
const responseJson = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
errors.push({ index, response: await response.json() });
|
||||
errors.push({ index, response: responseJson });
|
||||
} else {
|
||||
cache ??= responseJson.cache;
|
||||
}
|
||||
|
||||
index++;
|
||||
|
|
@ -46,6 +50,7 @@ export async function POST(request: Request) {
|
|||
processed: body.length - errors.length,
|
||||
errors: errors.length,
|
||||
details: errors,
|
||||
cache,
|
||||
});
|
||||
} catch (e) {
|
||||
return serverError(e);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ const schema = z.object({
|
|||
userAgent: z.string().optional(),
|
||||
timestamp: z.coerce.number().int().optional(),
|
||||
id: z.string().optional(),
|
||||
browser: z.string().optional(),
|
||||
os: z.string().optional(),
|
||||
device: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
data => {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ export function LoginForm() {
|
|||
onSuccess: async ({ token, user }) => {
|
||||
setClientAuthToken(token);
|
||||
setUser(user);
|
||||
|
||||
router.push('/websites');
|
||||
router.push('/');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { LAST_TEAM_CONFIG } from '@/lib/constants';
|
||||
import { getItem, removeItem } from '@/lib/storage';
|
||||
import { getItem } from '@/lib/storage';
|
||||
|
||||
export default function RootPage() {
|
||||
useEffect(() => {
|
||||
|
|
@ -11,8 +11,6 @@ export default function RootPage() {
|
|||
if (lastTeam) {
|
||||
redirect(`/teams/${lastTeam}/websites`);
|
||||
} else {
|
||||
removeItem(LAST_TEAM_CONFIG);
|
||||
|
||||
redirect(`/websites`);
|
||||
}
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { Column, useTheme } from '@umami/react-zen';
|
||||
import { useEffect } from 'react';
|
||||
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
||||
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
||||
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||
|
|
@ -10,6 +11,16 @@ import { Header } from './Header';
|
|||
|
||||
export function SharePage({ shareId }) {
|
||||
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window?.location?.href);
|
||||
const theme = url.searchParams.get('theme');
|
||||
|
||||
if (theme === 'light' || theme === 'dark') {
|
||||
setTheme(theme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isLoading || !shareToken) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { Icon, Row, Text } from '@umami/react-zen';
|
||||
import Link from 'next/link';
|
||||
import Link, { type LinkProps } from 'next/link';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ExternalLink as LinkIcon } from '@/components/icons';
|
||||
|
||||
export function ExternalLink({ href, children, ...props }) {
|
||||
export function ExternalLink({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: LinkProps & { href: string; children: ReactNode }) {
|
||||
return (
|
||||
<Row alignItems="center" overflow="hidden" gap>
|
||||
<Text title={href} truncate>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface LinkButtonProps extends ButtonProps {
|
|||
scroll?: boolean;
|
||||
variant?: any;
|
||||
prefetch?: boolean;
|
||||
asAnchor?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
|
|
@ -19,15 +20,22 @@ export function LinkButton({
|
|||
target,
|
||||
prefetch,
|
||||
children,
|
||||
asAnchor,
|
||||
...props
|
||||
}: LinkButtonProps) {
|
||||
const { dir } = useLocale();
|
||||
|
||||
return (
|
||||
<Button {...props} variant={variant} asChild>
|
||||
<Link href={href} dir={dir} scroll={scroll} target={target} prefetch={prefetch}>
|
||||
{children}
|
||||
</Link>
|
||||
{asAnchor ? (
|
||||
<a href={href} target={target}>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={href} dir={dir} scroll={scroll} target={target} prefetch={prefetch}>
|
||||
{children}
|
||||
</Link>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Column, Grid, Heading, Icon, Row, Text } from '@umami/react-zen';
|
||||
import type { ReactNode } from 'react';
|
||||
import { LinkButton } from './LinkButton';
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
|
|
@ -7,6 +8,7 @@ export function PageHeader({
|
|||
label,
|
||||
icon,
|
||||
showBorder = true,
|
||||
titleHref,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
|
|
@ -14,6 +16,7 @@ export function PageHeader({
|
|||
label?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
showBorder?: boolean;
|
||||
titleHref?: string;
|
||||
allowEdit?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
|
|
@ -33,7 +36,13 @@ export function PageHeader({
|
|||
{icon}
|
||||
</Icon>
|
||||
)}
|
||||
{title && <Heading size={{ xs: '2', md: '3', lg: '4' }}>{title}</Heading>}
|
||||
{title && titleHref ? (
|
||||
<LinkButton href={titleHref} variant="quiet">
|
||||
<Heading size={{ xs: '2', md: '3', lg: '4' }}>{title}</Heading>
|
||||
</LinkButton>
|
||||
) : (
|
||||
title && <Heading size={{ xs: '2', md: '3', lg: '4' }}>{title}</Heading>
|
||||
)}
|
||||
</Row>
|
||||
{description && (
|
||||
<Text color="muted" truncate style={{ maxWidth: 600 }} title={description}>
|
||||
|
|
@ -41,7 +50,9 @@ export function PageHeader({
|
|||
</Text>
|
||||
)}
|
||||
</Column>
|
||||
<Row justifyContent="flex-end">{children}</Row>
|
||||
<Row justifyContent="flex-end" alignItems="center">
|
||||
{children}
|
||||
</Row>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
Text,
|
||||
} from '@umami/react-zen';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import type { Key } from 'react';
|
||||
import {
|
||||
useConfig,
|
||||
useLoginQuery,
|
||||
|
|
@ -33,7 +34,8 @@ import {
|
|||
Users,
|
||||
} from '@/components/icons';
|
||||
import { Switch } from '@/components/svg';
|
||||
import { DOCS_URL } from '@/lib/constants';
|
||||
import { DOCS_URL, LAST_TEAM_CONFIG } from '@/lib/constants';
|
||||
import { removeItem } from '@/lib/storage';
|
||||
|
||||
export interface TeamsButtonProps {
|
||||
showText?: boolean;
|
||||
|
|
@ -44,7 +46,7 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
|
|||
const { user } = useLoginQuery();
|
||||
const { cloudMode } = useConfig();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { teamId } = useNavigation();
|
||||
const { teamId, router } = useNavigation();
|
||||
const { isMobile } = useMobile();
|
||||
const team = user?.teams?.find(({ id }) => id === teamId);
|
||||
const selectedKeys = new Set([teamId || 'user']);
|
||||
|
|
@ -54,7 +56,16 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
|
|||
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
|
||||
};
|
||||
|
||||
const handleAction = async () => {};
|
||||
const handleAction = async (key: Key) => {
|
||||
if (key === 'user') {
|
||||
removeItem(LAST_TEAM_CONFIG);
|
||||
if (cloudMode) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuTrigger>
|
||||
|
|
@ -84,16 +95,16 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
|
|||
</Pressable>
|
||||
<Popover placement="bottom start">
|
||||
<Column minWidth="300px">
|
||||
<Menu autoFocus="last" onAction={handleAction}>
|
||||
<Menu autoFocus="last">
|
||||
<SubmenuTrigger>
|
||||
<MenuItem id="teams" showChecked={false} showSubMenuIcon>
|
||||
<IconLabel icon={<Switch />} label={formatMessage(labels.switchAccount)} />
|
||||
</MenuItem>
|
||||
<Popover placement={isMobile ? 'bottom start' : 'right top'}>
|
||||
<Column minWidth="300px">
|
||||
<Menu selectionMode="single" selectedKeys={selectedKeys}>
|
||||
<Menu selectionMode="single" selectedKeys={selectedKeys} onAction={handleAction}>
|
||||
<MenuSection title={formatMessage(labels.myAccount)}>
|
||||
<MenuItem id="user" href={getUrl('/')}>
|
||||
<MenuItem id="user">
|
||||
<IconLabel icon={<User />} label={user.username} />
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Icon, ListItem, Row, Select, Text } from '@umami/react-zen';
|
||||
import { isAfter } from 'date-fns';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useDateRange, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { ChevronRight } from '@/components/icons';
|
||||
import { getDateRangeValue } from '@/lib/date';
|
||||
|
|
@ -45,13 +45,9 @@ export function WebsiteDateFilter({
|
|||
}
|
||||
};
|
||||
|
||||
const handleIncrement = useCallback(
|
||||
(increment: number) => {
|
||||
router.push(updateParams({ offset: +offset + increment }));
|
||||
},
|
||||
[offset],
|
||||
);
|
||||
|
||||
const handleIncrement = increment => {
|
||||
router.push(updateParams({ offset: Number(offset) + increment }));
|
||||
};
|
||||
const handleSelect = (compare: any) => {
|
||||
router.push(updateParams({ compare }));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { StatusLight, Text } from '@umami/react-zen';
|
||||
import { useMemo } from 'react';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { useActyiveUsersQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
export function ActiveUsers({
|
||||
|
|
@ -27,10 +28,12 @@ export function ActiveUsers({
|
|||
}
|
||||
|
||||
return (
|
||||
<StatusLight variant="success">
|
||||
<Text size="2" weight="medium">
|
||||
{count} {formatMessage(labels.online)}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
<LinkButton href={`/websites/${websiteId}/realtime`} variant="quiet">
|
||||
<StatusLight variant="success">
|
||||
<Text size="2" weight="medium">
|
||||
{count} {formatMessage(labels.online)}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ export function Legend({
|
|||
return (
|
||||
<Row key={text} onClick={() => onClick(item)}>
|
||||
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
|
||||
<Text size="2" color={hidden ? 'disabled' : undefined} wrap="nowrap">
|
||||
<Text
|
||||
size="2"
|
||||
color={hidden ? 'disabled' : undefined}
|
||||
truncate={true}
|
||||
style={{ maxWidth: '300px' }}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
|
|||
const endDate = startOfMinute(new Date());
|
||||
const startDate = subMinutes(endDate, REALTIME_RANGE);
|
||||
const prevEndDate = useRef(endDate);
|
||||
const prevData = useRef<string | null>(null);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!data) {
|
||||
|
|
@ -28,14 +29,22 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
|
|||
};
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
// Don't animate the bars shifting over because it looks weird
|
||||
const animationDuration = useMemo(() => {
|
||||
// Don't animate the bars shifting over because it looks weird
|
||||
if (isBefore(prevEndDate.current, endDate)) {
|
||||
prevEndDate.current = endDate;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Don't animate when data hasn't changed
|
||||
const serialized = JSON.stringify(chartData);
|
||||
if (prevData.current === serialized) {
|
||||
return 0;
|
||||
}
|
||||
prevData.current = serialized;
|
||||
|
||||
return DEFAULT_ANIMATION_DURATION;
|
||||
}, [endDate]);
|
||||
}, [endDate, chartData]);
|
||||
|
||||
return (
|
||||
<PageviewsChart
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@
|
|||
"label.websites": "المواقع",
|
||||
"label.window": "النافذة",
|
||||
"label.yesterday": "الأمس",
|
||||
"label.behavior": "السلوك",
|
||||
"message.action-confirmation": "اكتب {confirmation} في المربع أدناه للتأكيد.",
|
||||
"message.active-users": "{x} حاليا {x, plural, one {زائر واحد} other {زوار}}",
|
||||
"message.bad-request": "Bad request",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Сярэдняе",
|
||||
"label.back": "Назад",
|
||||
"label.before": "Да",
|
||||
"label.behavior": "Паводзіны",
|
||||
"label.boards": "Дошкі",
|
||||
"label.bounce-rate": "Паказчык адмоваў",
|
||||
"label.breakdown": "Разбіўка",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Средно",
|
||||
"label.back": "Назад",
|
||||
"label.before": "Преди",
|
||||
"label.behavior": "Поведение",
|
||||
"label.boards": "Дъски",
|
||||
"label.bounce-rate": "Kоефициент на отказ",
|
||||
"label.breakdown": "Разбивка",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "গড়",
|
||||
"label.back": "পেছনে",
|
||||
"label.before": "পূর্বে",
|
||||
"label.behavior": "আচরণ",
|
||||
"label.boards": "বোর্ডসমূহ",
|
||||
"label.bounce-rate": "উপরে উঠার হার",
|
||||
"label.breakdown": "ভাঙ্গন",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Prosjek",
|
||||
"label.back": "Nazad",
|
||||
"label.before": "Prije",
|
||||
"label.behavior": "Ponašanje",
|
||||
"label.boards": "Ploče",
|
||||
"label.bounce-rate": "Stopa napuštanja",
|
||||
"label.breakdown": "Pregled po kategorijama",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Mitjana",
|
||||
"label.back": "Enrere",
|
||||
"label.before": "Abans",
|
||||
"label.behavior": "Comportament",
|
||||
"label.boards": "Taulers",
|
||||
"label.bounce-rate": "Percentatge de rebot",
|
||||
"label.breakdown": "Desglossament",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Průměr",
|
||||
"label.back": "Zpět",
|
||||
"label.before": "Před",
|
||||
"label.behavior": "Chování",
|
||||
"label.boards": "Nástěnky",
|
||||
"label.bounce-rate": "Okamžité opuštění",
|
||||
"label.breakdown": "Rozpis",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Gennemsnit",
|
||||
"label.back": "Tilbage",
|
||||
"label.before": "Før",
|
||||
"label.behavior": "Adfærd",
|
||||
"label.boards": "Tavler",
|
||||
"label.bounce-rate": "Afvisningsprocent",
|
||||
"label.breakdown": "Opdeling",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Durchschnitt",
|
||||
"label.back": "Zrugg",
|
||||
"label.before": "Vor",
|
||||
"label.behavior": "Verhalte",
|
||||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "Absprungsrate",
|
||||
"label.breakdown": "Uufschlüsselig",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Durchschnitt",
|
||||
"label.back": "Zurück",
|
||||
"label.before": "Vor",
|
||||
"label.behavior": "Verhalten",
|
||||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "Absprungrate",
|
||||
"label.breakdown": "Aufschlüsselung",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "Ποσοστό αναπήδησης",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.behavior": "Συμπεριφορά",
|
||||
"label.browser": "Browser",
|
||||
"label.browsers": "Προγράμματα περιήγησης",
|
||||
"label.campaigns": "Campaigns",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Average",
|
||||
"label.back": "Back",
|
||||
"label.before": "Before",
|
||||
"label.behavior": "Behavior",
|
||||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "Bounce rate",
|
||||
"label.breakdown": "Breakdown",
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@
|
|||
"label.websites": "Websites",
|
||||
"label.window": "Window",
|
||||
"label.yesterday": "Yesterday",
|
||||
"label.behavior": "Behavior",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
|
||||
"message.bad-request": "Bad request",
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@
|
|||
"label.websites": "Sitios web",
|
||||
"label.window": "Ventana",
|
||||
"label.yesterday": "Ayer",
|
||||
"label.behavior": "Comportamiento",
|
||||
"message.action-confirmation": "Escriba {confirmation} en el cuadro a continuación para confirmar.",
|
||||
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
|
||||
"message.bad-request": "Bad request",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "میانگین",
|
||||
"label.back": "بازگشت",
|
||||
"label.before": "قبل از",
|
||||
"label.behavior": "رفتار",
|
||||
"label.boards": "بردها",
|
||||
"label.bounce-rate": "نرخ ریزش",
|
||||
"label.breakdown": "تفکیک",
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@
|
|||
"label.websites": "Verkkosivut",
|
||||
"label.window": "Window",
|
||||
"label.yesterday": "Yesterday",
|
||||
"label.behavior": "Behavior",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"message.active-users": "{x} {x, plural, one {vierailija} other {vierailijaa}}",
|
||||
"message.bad-request": "Bad request",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Miðal",
|
||||
"label.back": "Aftur",
|
||||
"label.before": "Áðrenn",
|
||||
"label.behavior": "Atferð",
|
||||
"label.boards": "Borð",
|
||||
"label.bounce-rate": "Bounce prosenttal",
|
||||
"label.breakdown": "Sundurgreining",
|
||||
|
|
|
|||
|
|
@ -289,6 +289,9 @@
|
|||
"label.websites": "Sites",
|
||||
"label.window": "Fenêtre",
|
||||
"label.yesterday": "Hier",
|
||||
"label.behavior": "Comportement",
|
||||
"label.traffic": "Trafic",
|
||||
"label.segments": "Segments",
|
||||
"message.action-confirmation": "Taper {confirmation} ci-dessous pour confirmer.",
|
||||
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
|
||||
"message.bad-request": "Bad request",
|
||||
|
|
@ -315,13 +318,13 @@
|
|||
"message.no-teams": "Vous n'avez pas créé d'équipe.",
|
||||
"message.no-users": "Aucun utilisateur.",
|
||||
"message.no-websites-configured": "Vous n'avez pas configuré de site.",
|
||||
"message.not-found": "Not found",
|
||||
"message.nothing-selected": "Nothing selected.",
|
||||
"message.not-found": "Non trouvé!",
|
||||
"message.nothing-selected": "Rien n'est sélectionné.",
|
||||
"message.page-not-found": "Page non trouvée.",
|
||||
"message.reset-website": "Pour réinitialiser ce site, taper {confirmation} ci-dessous pour confirmer.",
|
||||
"message.reset-website-warning": "Toutes les statistiques pour ce site seront supprimées, mais votre code de suivi restera intact.",
|
||||
"message.saved": "Enregistré.",
|
||||
"message.sever-error": "Server error",
|
||||
"message.sever-error": "Erreur serveur",
|
||||
"message.share-url": "Les statistiques de votre site sont accessibles publiquement sur cette URL :",
|
||||
"message.team-already-member": "Vous êtes déjà membre de cette équipe.",
|
||||
"message.team-not-found": "Équipe non trouvée.",
|
||||
|
|
@ -331,7 +334,7 @@
|
|||
"message.transfer-user-website-to-team": "Choisir l'équipe à laquelle transférer ce site.",
|
||||
"message.transfer-website": "Transférer la propriété du site sur votre compte ou à une autre équipe.",
|
||||
"message.triggered-event": "Évènement déclenché",
|
||||
"message.unauthorized": "Unauthorized",
|
||||
"message.unauthorized": "Non authorisé!",
|
||||
"message.user-deleted": "Utilisateur supprimé.",
|
||||
"message.viewed-page": "Page vue",
|
||||
"message.visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}"
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@
|
|||
"label.websites": "Sitios web",
|
||||
"label.window": "Ventá",
|
||||
"label.yesterday": "Onte",
|
||||
"label.behavior": "Comportamento",
|
||||
"message.action-confirmation": "Escribe {confirmation} na caixa de embaixo para confirmar.",
|
||||
"message.active-users": "{x} actual {x, plural, one {visitante} other {visitantes}}",
|
||||
"message.bad-request": "Bad request",
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@
|
|||
"label.value": "Value",
|
||||
"label.view": "View",
|
||||
"label.view-details": "פרטים נוספים",
|
||||
"label.behavior": "התנהגות",
|
||||
"label.view-only": "View only",
|
||||
"label.views": "צפיות",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "औसत",
|
||||
"label.back": "पीछे",
|
||||
"label.before": "पहले",
|
||||
"label.behavior": "व्यवहार",
|
||||
"label.boards": "बोर्ड्स",
|
||||
"label.bounce-rate": "उछाल दर",
|
||||
"label.breakdown": "विभाजन",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Prosjek",
|
||||
"label.back": "Natrag ",
|
||||
"label.before": "Prije",
|
||||
"label.behavior": "Ponašanje",
|
||||
"label.boards": "Ploče",
|
||||
"label.bounce-rate": "Stopa napuštanja",
|
||||
"label.breakdown": "Raspad",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Átlag",
|
||||
"label.back": "Vissza",
|
||||
"label.before": "Előtt",
|
||||
"label.behavior": "Viselkedés",
|
||||
"label.boards": "Táblák",
|
||||
"label.bounce-rate": "Visszafordulási arány",
|
||||
"label.breakdown": "Bontás",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Rata-rata",
|
||||
"label.back": "Kembali",
|
||||
"label.before": "Sebelum",
|
||||
"label.behavior": "Perilaku",
|
||||
"label.boards": "Papan",
|
||||
"label.bounce-rate": "Rasio pentalan",
|
||||
"label.breakdown": "Rincian",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Media",
|
||||
"label.back": "Indietro",
|
||||
"label.before": "Prima",
|
||||
"label.behavior": "Comportamento",
|
||||
"label.boards": "Bacheche",
|
||||
"label.bounce-rate": "Frequenza di rimbalzo",
|
||||
"label.breakdown": "Dettaglio",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "平均",
|
||||
"label.back": "戻る",
|
||||
"label.before": "直前",
|
||||
"label.behavior": "行動",
|
||||
"label.boards": "ボード",
|
||||
"label.bounce-rate": "直帰率",
|
||||
"label.breakdown": "故障",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "ជាមធ្យម",
|
||||
"label.back": "ថយក្រោយ",
|
||||
"label.before": "មុន",
|
||||
"label.behavior": "អាកប្បកិរិយា",
|
||||
"label.boards": "ក្តារ",
|
||||
"label.bounce-rate": "ចំនួនវិលត្រឡប់",
|
||||
"label.breakdown": "បំបែកលម្អិត",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "평균",
|
||||
"label.back": "뒤로",
|
||||
"label.before": "이전",
|
||||
"label.behavior": "행동",
|
||||
"label.boards": "보드",
|
||||
"label.bounce-rate": "이탈률",
|
||||
"label.breakdown": "세부 사항",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Vidurkis",
|
||||
"label.back": "Atgal",
|
||||
"label.before": "Prieš",
|
||||
"label.behavior": "Elgsena",
|
||||
"label.boards": "Lentos",
|
||||
"label.bounce-rate": "Atmetimo rodiklis",
|
||||
"label.breakdown": "Išskaidymas",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Дундаж",
|
||||
"label.back": "Буцах",
|
||||
"label.before": "Өмнө",
|
||||
"label.behavior": "Зан төлөв",
|
||||
"label.boards": "Самбарууд",
|
||||
"label.bounce-rate": "Нэг хуудас үзээд гарсан",
|
||||
"label.breakdown": "Задаргаа",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Average",
|
||||
"label.back": "Kembali",
|
||||
"label.before": "Before",
|
||||
"label.behavior": "Behavior",
|
||||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "Kadar lantunan",
|
||||
"label.breakdown": "Breakdown",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "ပျမ်းမျှ",
|
||||
"label.back": "နောက်သို့",
|
||||
"label.before": "မတိုင်မီ",
|
||||
"label.behavior": "အပြုအမူ",
|
||||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "Bounce နှုန်း",
|
||||
"label.breakdown": "ခွဲခြမ်းစိတ်ဖြာမှု",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Gjennomsnnitt",
|
||||
"label.back": "Tilbake",
|
||||
"label.before": "Før",
|
||||
"label.behavior": "Atferd",
|
||||
"label.boards": "Tavler",
|
||||
"label.bounce-rate": "Avvisningsfrekvens",
|
||||
"label.breakdown": "Nedbrytning",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Gemiddelde",
|
||||
"label.back": "Terug",
|
||||
"label.before": "Voor",
|
||||
"label.behavior": "Gedrag",
|
||||
"label.boards": "Borden",
|
||||
"label.bounce-rate": "Bouncepercentage",
|
||||
"label.breakdown": "Opsplitsen",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Średnia",
|
||||
"label.back": "Powrót",
|
||||
"label.before": "Przed",
|
||||
"label.behavior": "Zachowanie",
|
||||
"label.boards": "Tablice",
|
||||
"label.bounce-rate": "Współczynnik odrzuceń",
|
||||
"label.breakdown": "Rozbicie",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Média",
|
||||
"label.back": "Voltar",
|
||||
"label.before": "Antes",
|
||||
"label.behavior": "Comportamento",
|
||||
"label.boards": "Quadros",
|
||||
"label.bounce-rate": "Taxa de rejeição",
|
||||
"label.breakdown": "Detalhamento",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Média",
|
||||
"label.back": "Voltar",
|
||||
"label.before": "Antes",
|
||||
"label.behavior": "Comportamento",
|
||||
"label.boards": "Quadros",
|
||||
"label.bounce-rate": "Taxa de rejeição",
|
||||
"label.breakdown": "Detalhamento",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Mediu",
|
||||
"label.back": "Înapoi",
|
||||
"label.before": "Înainte",
|
||||
"label.behavior": "Comportament",
|
||||
"label.boards": "Panouri",
|
||||
"label.bounce-rate": "Rata de respingere",
|
||||
"label.breakdown": "Detaliat",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Средний",
|
||||
"label.back": "Назад",
|
||||
"label.before": "До",
|
||||
"label.behavior": "Поведение",
|
||||
"label.boards": "Доски",
|
||||
"label.bounce-rate": "Отказы",
|
||||
"label.breakdown": "Авария",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Average",
|
||||
"label.back": "ආපසු",
|
||||
"label.before": "Before",
|
||||
"label.behavior": "අචරණය",
|
||||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "Bounce rate",
|
||||
"label.breakdown": "Breakdown",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Priemer",
|
||||
"label.back": "Späť",
|
||||
"label.before": "Pred",
|
||||
"label.behavior": "Správanie",
|
||||
"label.boards": "Tabule",
|
||||
"label.bounce-rate": "Okamžité opustenie",
|
||||
"label.breakdown": "Rozpis",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Povprečno",
|
||||
"label.back": "Nazaj",
|
||||
"label.before": "Pred",
|
||||
"label.behavior": "Obnašanje",
|
||||
"label.boards": "Table",
|
||||
"label.bounce-rate": "Odbojna stopnja",
|
||||
"label.breakdown": "Razčlenitev",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Genomsnitt",
|
||||
"label.back": "Tillbaka",
|
||||
"label.before": "Före",
|
||||
"label.behavior": "Beteende",
|
||||
"label.boards": "Anslagstavlor",
|
||||
"label.bounce-rate": "Avvisningsfrekvens",
|
||||
"label.breakdown": "Analys",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"label.back": "பின்னால்",
|
||||
"label.before": "Before",
|
||||
"label.boards": "Boards",
|
||||
"label.behavior": "நடத்தை",
|
||||
"label.bounce-rate": "துள்ளல் விகிதம்",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.browser": "Browser",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Average",
|
||||
"label.back": "ย้อนกลับ",
|
||||
"label.before": "Before",
|
||||
"label.behavior": "พฤติกรรม",
|
||||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "อัตราตีกลับ",
|
||||
"label.breakdown": "Breakdown",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Ortalama",
|
||||
"label.back": "Geri",
|
||||
"label.before": "Önce",
|
||||
"label.behavior": "Davranış",
|
||||
"label.boards": "Panolar",
|
||||
"label.bounce-rate": "Tek sayfa ziyaret oranı",
|
||||
"label.breakdown": "Dağılım",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Середній",
|
||||
"label.back": "Назад",
|
||||
"label.before": "До",
|
||||
"label.behavior": "Поведінка",
|
||||
"label.boards": "Дошки",
|
||||
"label.bounce-rate": "Показник відмов",
|
||||
"label.breakdown": "Розподіл",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "Average",
|
||||
"label.back": "پیچھے",
|
||||
"label.before": "Before",
|
||||
"label.behavior": "رویے",
|
||||
"label.boards": "Boards",
|
||||
"label.bounce-rate": "اچھال کی شرح",
|
||||
"label.breakdown": "Breakdown",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"label.average": "Oʻrtacha",
|
||||
"label.back": "Orqaga",
|
||||
"label.before": "Oldin",
|
||||
"label.behavior": "Xulq-atvor",
|
||||
"label.bounce-rate": "Chiqib ketish darajasi",
|
||||
"label.breakdown": "Tahlil",
|
||||
"label.browser": "Brauzer",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"label.average": "Trung bình",
|
||||
"label.back": "Quay lại",
|
||||
"label.before": "Trước đó",
|
||||
"label.behavior": "Hành vi",
|
||||
"label.bounce-rate": "Tỷ lệ thoát trang",
|
||||
"label.breakdown": "Phân tích chi tiết",
|
||||
"label.browser": "Trình duyệt",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "平均",
|
||||
"label.back": "返回",
|
||||
"label.before": "之前",
|
||||
"label.behavior": "行为",
|
||||
"label.boards": "看板",
|
||||
"label.bounce-rate": "跳出率",
|
||||
"label.breakdown": "故障",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"label.average": "平均",
|
||||
"label.back": "返回",
|
||||
"label.before": "之前",
|
||||
"label.behavior": "行為",
|
||||
"label.boards": "看板",
|
||||
"label.bounce-rate": "跳出率",
|
||||
"label.breakdown": "細項分析",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import crypto from 'node:crypto';
|
||||
import { v4, v5 } from 'uuid';
|
||||
import { v4, v5, v7 } from 'uuid';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const IV_LENGTH = 16;
|
||||
|
|
@ -57,7 +57,9 @@ export function secret() {
|
|||
}
|
||||
|
||||
export function uuid(...args: any) {
|
||||
if (!args.length) return v4();
|
||||
if (args.length) {
|
||||
return v5(hash(...args, secret()), v5.DNS);
|
||||
}
|
||||
|
||||
return v5(hash(...args, secret()), v5.DNS);
|
||||
return process.env.USE_UUIDV7 ? v7() : v4();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,9 +114,9 @@ export async function getClientInfo(request: Request, payload: Record<string, an
|
|||
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(userAgent, payload?.screen);
|
||||
const browser = payload?.browser ?? browserName(userAgent);
|
||||
const os = payload?.os ?? (detectOS(userAgent) as string);
|
||||
const device = payload?.device ?? getDevice(userAgent, payload?.screen);
|
||||
|
||||
return { userAgent, browser, os, ip, country, region, city, device };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,6 +206,10 @@ async function rawQuery(sql: string, data: Record<string, any>, name?: string):
|
|||
return `$${params.length}${type ?? ''}`;
|
||||
});
|
||||
|
||||
if (process.env.DATABASE_REPLICA_URL && '$replica' in client) {
|
||||
return client.$replica().$queryRawUnsafe(query, ...params);
|
||||
}
|
||||
|
||||
return client.$queryRawUnsafe(query, ...params);
|
||||
}
|
||||
|
||||
|
|
@ -296,10 +300,6 @@ function getSchema() {
|
|||
}
|
||||
|
||||
function getClient() {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = process.env.DATABASE_URL;
|
||||
const replicaUrl = process.env.DATABASE_REPLICA_URL;
|
||||
const logQuery = process.env.LOG_QUERY;
|
||||
|
|
@ -307,43 +307,49 @@ function getClient() {
|
|||
const connectionUrl = new URL(url);
|
||||
const schema = connectionUrl.searchParams.get('schema') ?? undefined;
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: url.toString() }, { schema });
|
||||
const baseAdapter = new PrismaPg({ connectionString: url }, { schema });
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
adapter,
|
||||
const baseClient = new PrismaClient({
|
||||
adapter: baseAdapter,
|
||||
errorFormat: 'pretty',
|
||||
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
||||
});
|
||||
|
||||
if (replicaUrl) {
|
||||
const replicaAdapter = new PrismaPg({ connectionString: replicaUrl.toString() }, { schema });
|
||||
|
||||
const replicaClient = new PrismaClient({
|
||||
adapter: replicaAdapter,
|
||||
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
||||
});
|
||||
|
||||
prisma.$extends(
|
||||
readReplicas({
|
||||
replicas: [replicaClient],
|
||||
}),
|
||||
);
|
||||
if (logQuery) {
|
||||
baseClient.$on('query', log);
|
||||
}
|
||||
|
||||
if (!replicaUrl) {
|
||||
log('Prisma initialized');
|
||||
globalThis[PRISMA] ??= baseClient;
|
||||
return baseClient;
|
||||
}
|
||||
|
||||
const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, { schema });
|
||||
|
||||
const replicaClient = new PrismaClient({
|
||||
adapter: replicaAdapter,
|
||||
errorFormat: 'pretty',
|
||||
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
||||
});
|
||||
|
||||
if (logQuery) {
|
||||
prisma.$on('query' as never, log);
|
||||
replicaClient.$on('query', log);
|
||||
}
|
||||
|
||||
log('Prisma initialized');
|
||||
const extended = baseClient.$extends(
|
||||
readReplicas({
|
||||
replicas: [replicaClient],
|
||||
}),
|
||||
);
|
||||
|
||||
if (!globalThis[PRISMA]) {
|
||||
globalThis[PRISMA] = prisma;
|
||||
}
|
||||
log('Prisma initialized (with replica)');
|
||||
globalThis[PRISMA] ??= extended;
|
||||
|
||||
return prisma;
|
||||
return extended;
|
||||
}
|
||||
|
||||
const client: PrismaClient = globalThis[PRISMA] || getClient();
|
||||
const client = (globalThis[PRISMA] || getClient()) as ReturnType<typeof getClient>;
|
||||
|
||||
export default {
|
||||
client,
|
||||
|
|
|
|||
|
|
@ -137,6 +137,9 @@ export async function resetWebsite(websiteId: string) {
|
|||
|
||||
return transaction(
|
||||
[
|
||||
client.revenue.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.eventData.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
|
|
@ -175,35 +178,44 @@ export async function deleteWebsite(websiteId: string) {
|
|||
const { client, transaction } = prisma;
|
||||
const cloudMode = !!process.env.CLOUD_MODE;
|
||||
|
||||
return transaction([
|
||||
client.eventData.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.sessionData.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.websiteEvent.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.report.deleteMany({
|
||||
where: {
|
||||
websiteId,
|
||||
},
|
||||
}),
|
||||
cloudMode
|
||||
? client.website.update({
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
where: { id: websiteId },
|
||||
})
|
||||
: client.website.delete({
|
||||
where: { id: websiteId },
|
||||
}),
|
||||
]).then(async data => {
|
||||
return transaction(
|
||||
[
|
||||
client.revenue.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.eventData.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.sessionData.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.websiteEvent.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.report.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
client.segment.deleteMany({
|
||||
where: { websiteId },
|
||||
}),
|
||||
cloudMode
|
||||
? client.website.update({
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
where: { id: websiteId },
|
||||
})
|
||||
: client.website.delete({
|
||||
where: { id: websiteId },
|
||||
}),
|
||||
],
|
||||
{
|
||||
timeout: 30000,
|
||||
},
|
||||
).then(async data => {
|
||||
if (cloudMode) {
|
||||
await redis.client.del(`website:${websiteId}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue