Merge branch 'main' into feat-database-backups

This commit is contained in:
ItzNotABug
2024-09-11 15:00:10 +05:30
28 changed files with 239 additions and 163 deletions
+1
View File
@@ -41,6 +41,7 @@ jobs:
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
"SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}"
publish-cloud-stage:
runs-on: ubuntu-latest
steps:
+3
View File
@@ -150,3 +150,6 @@ dist
# SvelteKit build / generate output
.svelte-kit
# Sentry Config File
.sentryclirc
+3
View File
@@ -22,11 +22,14 @@ ARG PUBLIC_CONSOLE_MODE
ARG PUBLIC_APPWRITE_ENDPOINT
ARG PUBLIC_GROWTH_ENDPOINT
ARG PUBLIC_STRIPE_KEY
ARG SENTRY_AUTH_TOKEN
ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV NODE_OPTIONS=--max_old_space_size=8192
RUN pnpm run sync && pnpm run build
+1
View File
@@ -8,6 +8,7 @@ services:
PUBLIC_APPWRITE_ENDPOINT: ${PUBLIC_APPWRITE_ENDPOINT}
PUBLIC_GROWTH_ENDPOINT: ${PUBLIC_GROWTH_ENDPOINT}
PUBLIC_STRIPE_KEY: ${PUBLIC_STRIPE_KEY}
SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN}
develop:
watch:
- action: rebuild
+1
View File
@@ -23,6 +23,7 @@
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/pink-icons": "0.25.0",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.26.0",
"@stripe/stripe-js": "^3.5.0",
"ai": "^2.2.37",
"analytics": "^0.8.14",
+23 -10
View File
@@ -1,14 +1,27 @@
import * as Sentry from '@sentry/sveltekit';
import { AppwriteException } from '@appwrite.io/console';
import type { HandleClientError } from '@sveltejs/kit';
import { isCloud, isProd } from '$lib/system';
export const handleError: HandleClientError = async ({ error, message, status }) => {
if (error instanceof AppwriteException) {
status = error.code === 0 ? undefined : error.code;
message = error.message;
Sentry.init({
enabled: isCloud && isProd,
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976',
tracesSampleRate: 1,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 1,
integrations: [Sentry.replayIntegration()]
});
export const handleError: HandleClientError = Sentry.handleErrorWithSentry(
async ({ error, message, status }) => {
if (error instanceof AppwriteException) {
status = error.code === 0 ? undefined : error.code;
message = error.message;
}
return {
message,
status
};
}
return {
message,
status
};
};
);
+14
View File
@@ -0,0 +1,14 @@
import { sequence } from '@sveltejs/kit/hooks';
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
import { isCloud, isProd } from '$lib/system';
Sentry.init({
enabled: isCloud && isProd,
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976',
tracesSampleRate: 1.0
});
export const handle = sequence(sentryHandle());
export const handleError = handleErrorWithSentry();
@@ -1,7 +1,7 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { formatNum } from '$lib/helpers/string';
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
import { plansInfo, tierFree, tierPro, type Tier } from '$lib/stores/billing';
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
let selectedTab: Tier = BillingPlan.FREE;
@@ -23,11 +23,11 @@
on:click={() => (selectedTab = BillingPlan.PRO)}>
{tierPro.name}
</SecondaryTabsItem>
<SecondaryTabsItem
<!-- <SecondaryTabsItem
disabled={selectedTab === BillingPlan.SCALE}
on:click={() => (selectedTab = BillingPlan.SCALE)}>
{tierScale.name}
</SecondaryTabsItem>
</SecondaryTabsItem> -->
</SecondaryTabs>
</div>
+5 -1
View File
@@ -45,7 +45,11 @@
};
</script>
<div class="table-with-scroll {classes}" class:u-margin-block-start-16={!noMargin} data-private>
<div
class="table-with-scroll {classes}"
style:border-radius={noStyles ? '0' : ''}
class:u-margin-block-start-16={!noMargin}
data-private>
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
<table
class="table"
+2 -1
View File
@@ -79,7 +79,7 @@ const createUploader = () => {
(p) => {
newFile.$id = p.$id;
newFile.progress = p.progress;
newFile.completed = p.progress === 100 ? true : false;
newFile.completed = p.progress === 100;
updateFile(p.$id, newFile);
}
);
@@ -91,6 +91,7 @@ const createUploader = () => {
removeFromQueue: (id: string) => {
update((n) => {
n.files = n.files.filter((f) => f.$id !== id);
n.isOpen = n.files.length !== 0;
return n;
});
},
+2
View File
@@ -23,5 +23,7 @@ export const ENV = {
export const MODE = VARS.CONSOLE_MODE === Mode.CLOUD ? Mode.CLOUD : Mode.SELF_HOSTED;
export const isCloud = MODE === Mode.CLOUD;
export const isSelfHosted = MODE !== Mode.CLOUD;
export const isDev = ENV.DEV;
export const isProd = ENV.PROD;
export const hasStripePublicKey = !!VARS.PUBLIC_STRIPE_KEY;
export const GRACE_PERIOD_OVERRIDE = false;
@@ -50,7 +50,7 @@
const callbackState = {
from: 'github',
to: 'template',
step: '4',
step: '5',
template: $template.id,
templateConfig: JSON.stringify($templateConfig)
};
+5
View File
@@ -7,6 +7,7 @@
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorType, type Models } from '@appwrite.io/console';
import QrFrame from '$lib/images/qr2.svg';
import { addNotification } from '$lib/stores/notifications';
export let showSetup = false;
@@ -28,6 +29,10 @@
await sdk.forConsole.account.updateMfaAuthenticator(AuthenticatorType.Totp, code);
await Promise.all([invalidate(Dependencies.ACCOUNT), invalidate(Dependencies.FACTORS)]);
showSetup = false;
addNotification({
type: 'success',
message: 'Authenticator app connected successfully'
});
trackEvent(Submit.AccountAuthenticatorUpdate);
} catch (e) {
error = e.message;
+11 -6
View File
@@ -109,13 +109,18 @@
<div
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
style="padding-block-end: 16px">
<div class="u-flex u-gap-8">
<div class="u-flex u-gap-8 u-cross-baseline">
<div class="avatar is-size-x-small">
<span class="icon-device-mobile" aria-hidden="true" />
</div>
<div class="u-flex-vertical u-gap-4 body-text-2">
<span class="u-bold">Authenticator app</span>
<span
<div class="u-flex-vertical u-gap-4">
<div class="u-flex u-gap-4 u-cross-center">
<span class="body-text-2 u-bold">Authenticator app</span>
{#if $factors.totp}
<Pill>connected</Pill>
{/if}
</div>
<span class="body-text-2"
>Use an authentication app to generate two-factor authentication
codes.</span>
</div>
@@ -141,7 +146,7 @@
<div
class="u-flex u-main-space-between u-sep-block-end"
style="padding-block-end: 16px">
<div class="u-flex u-gap-8">
<div class="u-flex u-gap-8 u-cross-baseline">
<div class="avatar is-size-x-small">
<span class="icon-mail" aria-hidden="true" />
</div>
@@ -185,7 +190,7 @@
<div
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
style="padding-block-end: 16px">
<div class="u-flex u-gap-8">
<div class="u-flex u-gap-8 u-cross-baseline">
<div class="avatar is-size-x-small">
<span class="icon-lock-open" aria-hidden="true" />
</div>
+4 -4
View File
@@ -32,11 +32,11 @@
{
value: BillingPlan.PRO,
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons`
},
{
value: BillingPlan.SCALE,
label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
}
// {
// value: BillingPlan.SCALE,
// label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
// }
]
: [];
@@ -149,9 +149,9 @@
type: 'success',
isHtml: true,
message: `
<b>${$organization.name}</b> has been changed to ${
tierToPlan(billingPlan).name
} plan.`
<b>${$organization.name}</b> will change to ${
tierToPlan(billingPlan).name
} plan at the end of the current billing cycle.`
});
trackEvent(Submit.OrganizationDowngrade, {
@@ -162,7 +162,7 @@
type: 'error',
message: e.message
});
trackError(e, isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade);
trackError(e, Submit.OrganizationDowngrade);
}
}
@@ -213,22 +213,12 @@
await invalidate(Dependencies.ORGANIZATION);
await goto(`${base}/organization-${org.$id}`);
if (isUpgrade) {
addNotification({
type: 'success',
message: 'Your organization has been upgraded'
});
} else {
addNotification({
type: 'success',
isHtml: true,
message: `
<b>${$organization.name}</b> will change to ${
tierToPlan(billingPlan).name
} plan at the end of the current billing cycle.`
});
}
trackEvent(isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade, {
addNotification({
type: 'success',
message: 'Your organization has been upgraded'
});
trackEvent(Submit.OrganizationUpgrade, {
plan: tierToPlan(billingPlan)?.name
});
} catch (e) {
@@ -236,7 +226,7 @@
type: 'error',
message: e.message
});
trackError(e, isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade);
trackError(e, Submit.OrganizationUpgrade);
}
}
@@ -324,6 +314,7 @@
bind:value={feedbackDowngradeReason} />
<InputTextarea
id="comment"
required
label="If you need to elaborate, please do so here"
placeholder="Enter feedback"
bind:value={feedbackMessage} />
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Alert, Modal } from '$lib/components';
import { InputText, InputEmail, Button, FormList } from '$lib/elements/forms';
@@ -17,7 +18,7 @@
const dispatch = createEventDispatcher();
const url = `${$page.url.origin}/invite`;
const url = `${$page.url.origin}${base}/invite`;
$: plan = $plansInfo?.get($organization?.billingPlan);
let email: string, name: string, error: string;
@@ -1,6 +1,7 @@
<script lang="ts">
import { Box, CardGrid, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { base } from '$app/paths';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackEvent } from '$lib/actions/analytics';
@@ -43,7 +44,7 @@
external
class="u-margin-block-start-16"
on:click={downloadPdf}
href="/legal/dpa.pdf"
href="{base}/legal/dpa.pdf"
event="download_dpa">
<span class="icon-download" aria-hidden="true" />
<span class="text">Download</span>
@@ -149,7 +149,9 @@
}
}
]} />
<ProjectBreakdown projects={project} metric="bandwidth" {data} />
{#if project?.length > 0}
<ProjectBreakdown projects={project} metric="bandwidth" {data} />
{/if}
{:else}
<Card isDashed>
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
@@ -198,7 +200,9 @@
)
}
]} />
<ProjectBreakdown projects={project} metric="users" {data} />
{#if project?.length > 0}
<ProjectBreakdown projects={project} metric="users" {data} />
{/if}
{:else}
<Card isDashed>
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
@@ -248,7 +252,9 @@
]
}
]} />
<ProjectBreakdown projects={project} metric="executions" {data} />
{#if project?.length > 0}
<ProjectBreakdown projects={project} metric="executions" {data} />
{/if}
{:else}
<Card isDashed>
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
@@ -284,7 +290,9 @@
progressValue={bytesToSize(current, 'GB')}
progressMax={max}
minimum={1} />
<ProjectBreakdown projects={project} metric="storage" {data} />
{#if project?.length > 0}
<ProjectBreakdown projects={project} metric="storage" {data} />
{/if}
{:else}
<Card isDashed>
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
@@ -5,6 +5,7 @@
import { Button, Form, FormItem, FormItemPart } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { upgradeURL } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
@@ -32,7 +33,7 @@
? 'Available on Appwrite Cloud'
: 'Upgrade to add mock phone numbers';
let emptyStateDescription: string = isSelfHosted
? 'Sign up to Cloud to add mock phone numbers to your projects.'
? 'Sign up for Cloud to add mock phone numbers to your projects.'
: 'Upgrade to a Pro plan to add mock phone numbers to your project.';
let cta: string = isSelfHosted ? 'Sign up' : 'Upgrade plan';
@@ -137,8 +138,8 @@
class="u-margin-block-start-32"
secondary
fullWidth
href="https://cloud.appwrite.io/register"
external
external={isSelfHosted}
href={isCloud ? $upgradeURL : 'https://cloud.appwrite.io/register'}
on:click={() => {
trackEvent('click_cloud_signup', {
from: 'button',
@@ -130,7 +130,7 @@
>here</a
>.
</p>
<div class="u-margin-block-start-16 u-flex u-gap-8">
<div class="u-margin-block-start-16 u-flex u-flex-wrap u-gap-8">
<slot />
</div>
</li>
@@ -18,7 +18,7 @@
</script>
<Box radius="small" class="u-flex u-gap-16 u-cross-center">
<div class="u-un-break-text">
<div class="u-un-break-text is-not-mobile">
<span class="icon-translate" />
<span class="text">Template language</span>
</div>
@@ -44,30 +44,11 @@
$: hasValidAttributes = $collection?.attributes?.some((attr) => attr.status === 'available');
</script>
<Container>
<div class="heading-grid u-main-justify-between u-cross-center">
<Heading tag="h2" size="5">Documents</Heading>
<div class="u-flex u-main-end is-only-mobile">
<Button
disabled={!(hasAttributes && hasValidAttributes)}
on:click={openWizard}
event="create_document">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create document</span>
</Button>
</div>
<Filters query={data.query} {columns} disabled={!(hasAttributes && hasValidAttributes)} />
<div class="u-flex u-main-end u-gap-16">
<ViewSelector
view={data.view}
{columns}
isCustomCollection
hideView
allowNoColumns
showColsTextMobile />
<div class="is-not-mobile">
{#key $page.params.collection}
<Container>
<div class="heading-grid u-main-justify-between u-cross-center">
<Heading tag="h2" size="5">Documents</Heading>
<div class="u-flex u-main-end is-only-mobile">
<Button
disabled={!(hasAttributes && hasValidAttributes)}
on:click={openWizard}
@@ -76,76 +57,100 @@
<span class="text">Create document</span>
</Button>
</div>
<Filters
query={data.query}
{columns}
disabled={!(hasAttributes && hasValidAttributes)} />
<div class="u-flex u-main-end u-gap-16">
<ViewSelector
view={data.view}
{columns}
isCustomCollection
hideView
allowNoColumns
showColsTextMobile />
<div class="is-not-mobile">
<Button
disabled={!(hasAttributes && hasValidAttributes)}
on:click={openWizard}
event="create_document">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create document</span>
</Button>
</div>
</div>
</div>
</div>
{#if hasAttributes && hasValidAttributes}
{#if data.documents.total}
<Table {data} />
{#if hasAttributes && hasValidAttributes}
{#if data.documents.total}
<Table {data} />
<PaginationWithLimit
name="Documents"
limit={data.limit}
offset={data.offset}
total={data.documents.total} />
{:else if $hasPageQueries}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2 u-bold">Sorry, we couldn't find any documents.</b>
<p>There are no documents that match your filters.</p>
<PaginationWithLimit
name="Documents"
limit={data.limit}
offset={data.offset}
total={data.documents.total} />
{:else if $hasPageQueries}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2 u-bold">Sorry, we couldn't find any documents.</b>
<p>There are no documents that match your filters.</p>
</div>
<div class="u-flex common-section u-main-center">
<Button
secondary
on:click={() => {
queries.clearAll();
queries.apply();
}}>
Clear filters
</Button>
</div>
</div>
<div class="u-flex common-section u-main-center">
</EmptySearch>
{:else}
<Empty
single
href="https://appwrite.io/docs/products/databases/documents"
target="document"
on:click={openWizard} />
{/if}
{:else}
<Empty single target="attribute" on:click={() => (showCreateDropdown = true)}>
<div class="u-text-center">
<Heading size="7" tag="h2">Create an attribute to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
<Button
external
href="https://appwrite.io/docs/products/databases/collections#attributes"
text
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
<CreateAttributeDropdown
bind:showCreateDropdown
bind:showCreate={showCreateAttribute}
bind:selectedOption={selectedAttribute}>
<Button
secondary
event="create_attribute"
on:click={() => {
queries.clearAll();
queries.apply();
showCreateDropdown = !showCreateDropdown;
}}>
Clear filters
Create attribute
</Button>
</div>
</CreateAttributeDropdown>
</div>
</EmptySearch>
{:else}
<Empty
single
href="https://appwrite.io/docs/products/databases/documents"
target="document"
on:click={openWizard} />
</Empty>
{/if}
{:else}
<Empty single target="attribute" on:click={() => (showCreateDropdown = true)}>
<div class="u-text-center">
<Heading size="7" tag="h2">Create an attribute to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
<Button
external
href="https://appwrite.io/docs/products/databases/collections#attributes"
text
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
<CreateAttributeDropdown
bind:showCreateDropdown
bind:showCreate={showCreateAttribute}
bind:selectedOption={selectedAttribute}>
<Button
secondary
event="create_attribute"
on:click={() => {
showCreateDropdown = !showCreateDropdown;
}}>
Create attribute
</Button>
</CreateAttributeDropdown>
</div>
</Empty>
{/if}
</Container>
</Container>
{/key}
<CreateAttribute bind:showCreate={showCreateAttribute} bind:selectedOption={selectedAttribute} />
@@ -96,7 +96,7 @@
},
{
id: 'size',
title: 'Deployment size',
title: 'Size',
type: 'integer',
show: true,
width: 140,
@@ -8,14 +8,17 @@
</script>
<div class="u-flex u-gap-4 u-cross-center">
<Trim alternativeTrim>
<a href={`http://${domain.rules[0].domain}`} target="_blank">
<a
href={`http://${domain.rules[0].domain}`}
target="_blank"
class="u-flex u-gap-4 u-cross-center">
<Trim alternativeTrim>
<span class="link">
{domain.rules[0].domain}
</span>
</a>
</Trim>
<span class="icon-external-link" aria-hidden="true" />
</Trim>
<span class="icon-external-link" aria-hidden="true" />
</a>
{#if domain.rules.length > 1}
<DropList bind:show={showDropdown} scrollable>
<Pill button on:click={() => (showDropdown = !showDropdown)}>
@@ -4,7 +4,6 @@ import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
import { isCloud } from '$lib/system';
import type { OrganizationUsage } from '$lib/sdk/billing';
export const load: PageLoad = async ({ params, depends, url, route, parent }) => {
const { organization } = await parent();
@@ -13,20 +12,20 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
const search = getSearch(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
let organizationUsage: OrganizationUsage = null;
if (isCloud && organization?.$id) {
organizationUsage = await sdk.forConsole.billing.listUsage(organization.$id);
}
return {
offset,
limit,
search,
files: await sdk.forProject.storage.listFiles(
const [files, organizationUsage] = await Promise.all([
sdk.forProject.storage.listFiles(
params.bucket,
[Query.limit(limit), Query.offset(offset), Query.orderDesc('')],
search
),
isCloud && organization?.$id ? sdk.forConsole.billing.listUsage(organization.$id) : null
]);
return {
offset,
limit,
search,
files,
organizationUsage
};
};
@@ -28,17 +28,18 @@
});
async function create() {
const fileId = $createFile.id ?? ID.unique();
try {
uploader.uploadFile(
const uploadPromise = uploader.uploadFile(
bucketId,
$createFile.id ?? ID.unique(),
fileId,
$createFile.files[0],
$createFile.permissions
);
createFile.reset();
wizard.hide();
invalidate(Dependencies.FILES);
addNotification({
type: 'success',
@@ -47,7 +48,12 @@
trackEvent(Submit.FileCreate, {
customId: !!$createFile.id
});
await uploadPromise;
invalidate(Dependencies.FILES);
} catch (e) {
uploader.removeFromQueue(fileId);
addNotification({
type: 'error',
message: e.message
+11 -3
View File
@@ -1,16 +1,24 @@
import { defineConfig } from 'vitest/config';
import { sveltekit } from '@sveltejs/kit/vite';
import { sentrySvelteKit } from '@sentry/sveltekit';
const config = defineConfig({
plugins: [sveltekit()],
plugins: [
sentrySvelteKit({
adapter: 'auto',
sourceMapsUploadOptions: {
org: 'appwrite',
project: 'console'
}
}),
sveltekit()
],
optimizeDeps: {
include: ['echarts', 'prismjs']
},
ssr: {
noExternal: [
'@analytics/google-analytics',
'@sentry/browser',
'@sentry-internal/tracing',
'analytics',
'dayjs',
'echarts',