diff --git a/.github/workflows/dockerize-profiles.yml b/.github/workflows/dockerize-profiles.yml new file mode 100644 index 000000000..aafa95f32 --- /dev/null +++ b/.github/workflows/dockerize-profiles.yml @@ -0,0 +1,49 @@ +name: Dockerize Profiles + +on: + push: + branches: [feat-profiles] + pull_request: + types: [opened, synchronize, reopened] + branches: [feat-profiles] + workflow_dispatch: + +jobs: + dockerize-profiles: + runs-on: ubuntu-latest + + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: appwrite/console-profiles + tags: | + type=ref,event=branch,prefix=branch- + type=ref,event=pr + type=sha,prefix=sha- + type=raw,value=gh-${{ github.run_id}} + flavor: | + latest=false + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 44a6fcae7..821f0410a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,9 +31,16 @@ ARG PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS ARG PUBLIC_APPWRITE_ENDPOINT ARG PUBLIC_GROWTH_ENDPOINT ARG PUBLIC_STRIPE_KEY +ARG PUBLIC_SENTRY_DSN +ARG PUBLIC_SENTRY_INCLUDE_PII +ARG PUBLIC_SENTRY_ENVIRONMENT +ARG PUBLIC_SENTRY_TRACES_SAMPLE_RATE +ARG PUBLIC_SENTRY_REPLAY_SAMPLE_RATE +ARG PUBLIC_SENTRY_DEBUG ARG SENTRY_AUTH_TOKEN ARG SENTRY_RELEASE + ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE @@ -44,6 +51,12 @@ ENV PUBLIC_APPWRITE_MULTI_REGION=$PUBLIC_APPWRITE_MULTI_REGION ENV PUBLIC_CONSOLE_EMAIL_VERIFICATION=$PUBLIC_CONSOLE_EMAIL_VERIFICATION ENV PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=$PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY +ENV PUBLIC_SENTRY_DSN=$PUBLIC_SENTRY_DSN +ENV PUBLIC_SENTRY_INCLUDE_PII=$PUBLIC_SENTRY_INCLUDE_PII +ENV PUBLIC_SENTRY_ENVIRONMENT=$PUBLIC_SENTRY_ENVIRONMENT +ENV PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$PUBLIC_SENTRY_TRACES_SAMPLE_RATE +ENV PUBLIC_SENTRY_REPLAY_SAMPLE_RATE=$PUBLIC_SENTRY_REPLAY_SAMPLE_RATE +ENV PUBLIC_SENTRY_DEBUG=$PUBLIC_SENTRY_DEBUG ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN ENV SENTRY_RELEASE=$SENTRY_RELEASE ENV PUBLIC_IMAGINE_CDN_URL=$PUBLIC_IMAGINE_CDN_URL diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 4a6565025..2338c2170 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,20 +1,15 @@ -import * as Sentry from '@sentry/sveltekit'; -import { isCloud, isProd } from '$lib/system'; import { AppwriteException } from '@appwrite.io/console'; import type { HandleClientError } from '@sveltejs/kit'; +import { setupSentry } from '$lib/sentry'; -Sentry.init({ - enabled: isCloud && isProd, - dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976', - tracesSampleRate: 1, - replaysSessionSampleRate: 0, - replaysOnErrorSampleRate: 0 +setupSentry({ + withSessionReplay: true }); export const handleError: HandleClientError = ({ error, message, status }) => { console.error(error); - let type; + let type: string; if (error instanceof AppwriteException) { status = error.code === 0 ? undefined : error.code; message = error.message; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e54c183db..ba452c378 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,12 +1,9 @@ import { sequence } from '@sveltejs/kit/hooks'; import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit'; -import * as Sentry from '@sentry/sveltekit'; -import { isCloud, isProd } from '$lib/system'; +import { setupSentry } from '$lib/sentry'; -Sentry.init({ - enabled: isCloud && isProd, - dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976', - tracesSampleRate: 1.0 +setupSentry({ + withSessionReplay: false }); export const handle = sequence(sentryHandle()); diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts new file mode 100644 index 000000000..e7325e9ba --- /dev/null +++ b/src/lib/sentry.ts @@ -0,0 +1,64 @@ +import { env } from '$env/dynamic/public'; +import * as Sentry from '@sentry/sveltekit'; +import { isCloud } from './system'; + +const timestamp = Date.now(); + +export function getSessionId(userId: string) { + return `${userId}-${timestamp}`; +} + +export function identify(userId: string) { + if (isCloud) { + Sentry.setContext('Imagine Request Context', { + session_id: getSessionId(userId) + }); + Sentry.setUser({ + id: userId + }); + } +} + +export function setupSentry({ withSessionReplay }: { withSessionReplay: boolean }) { + const dsn = env.PUBLIC_SENTRY_DSN; + const environment = env.PUBLIC_SENTRY_ENVIRONMENT; + + if (!dsn) { + return; + } + + if (dsn && !environment) { + throw new Error( + "PUBLIC_SENTRY_ENVIRONMENT is required when PUBLIC_SENTRY_DSN is set. For local development, set it to your name, e.g. 'torsten'." + ); + } + + const variables = { + enabled: true, + includePII: env.PUBLIC_SENTRY_INCLUDE_PII === 'true', + tracesSampleRate: parseFloat(env.PUBLIC_SENTRY_TRACES_SAMPLE_RATE), + replaysSessionSampleRate: parseFloat(env.PUBLIC_SENTRY_REPLAY_SAMPLE_RATE), + debug: env.PUBLIC_SENTRY_DEBUG === 'true' + }; + + const integrations = []; + if (withSessionReplay) { + integrations.push( + Sentry.replayIntegration({ + maskAllText: variables.includePII ? false : true, + maskAllInputs: variables.includePII ? false : true + }) + ); + } + + Sentry.init({ + enabled: variables.enabled, + dsn, + tracesSampleRate: variables.tracesSampleRate, + replaysSessionSampleRate: variables.replaysSessionSampleRate, + replaysOnErrorSampleRate: 1, + integrations, + debug: variables.debug, + sendDefaultPii: true + }); +} diff --git a/src/lib/studio/monaco-style-manager.ts b/src/lib/studio/monaco-style-manager.ts new file mode 100644 index 000000000..3f71f8fe4 --- /dev/null +++ b/src/lib/studio/monaco-style-manager.ts @@ -0,0 +1,53 @@ +const MONACO_STYLE_ATTRIBUTE = 'data-appwrite-studio-monaco-style'; + +let monacoStyleObserver: MutationObserver | null = null; + +function findMonacoEditorCssLink(): HTMLLinkElement | null { + if (typeof document === 'undefined') { + return null; + } + + const link = document.head?.querySelector( + 'link[rel="stylesheet"][href*="monaco-editor"][href*="editor.main.css"]' + ); + + return link ?? null; +} + +function syncMonacoStyles(shadow: ShadowRoot) { + if (typeof document === 'undefined') { + return; + } + + // Check if already synced + if (shadow.querySelector(`[${MONACO_STYLE_ATTRIBUTE}]`)) { + return; + } + + const link = findMonacoEditorCssLink(); + if (!link) { + return; + } + + const clone = link.cloneNode(true) as HTMLLinkElement; + clone.setAttribute(MONACO_STYLE_ATTRIBUTE, 'true'); + shadow.appendChild(clone); +} + +export function ensureMonacoStyles(shadow: ShadowRoot) { + syncMonacoStyles(shadow); + + if ( + monacoStyleObserver || + typeof MutationObserver === 'undefined' || + typeof document === 'undefined' + ) { + return; + } + + monacoStyleObserver = new MutationObserver(() => { + syncMonacoStyles(shadow); + }); + + monacoStyleObserver.observe(document.head, { childList: true }); +} diff --git a/src/lib/studio/studio-widget.ts b/src/lib/studio/studio-widget.ts index d00a4686d..05dca3680 100644 --- a/src/lib/studio/studio-widget.ts +++ b/src/lib/studio/studio-widget.ts @@ -3,7 +3,9 @@ import { app } from '$lib/stores/app'; import { get } from 'svelte/store'; import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; +import { ensureMonacoStyles } from './monaco-style-manager'; import DEV_CSS_URL from '@imagine.dev/web-components/imagine-web-components.css?url'; +import { getSessionId } from '$lib/sentry'; const COMPONENT_SELECTOR = 'imagine-web-components-wrapper[data-appwrite-studio]'; const STYLE_ATTRIBUTE = 'data-appwrite-studio-style'; @@ -87,6 +89,7 @@ function injectStyles(node: HTMLElement, attempt = 0) { } if (shadow.querySelector(`link[${STYLE_ATTRIBUTE}]`)) { + ensureMonacoStyles(shadow); return; } @@ -95,6 +98,7 @@ function injectStyles(node: HTMLElement, attempt = 0) { link.href = DEV_OVERRIDE_WEB_COMPONENTS ? DEV_CSS_URL : CDN_CSS_URL; link.setAttribute(STYLE_ATTRIBUTE, 'true'); shadow.prepend(link); + ensureMonacoStyles(shadow); }) .catch(() => { /* no-op */ @@ -272,6 +276,7 @@ export function hideStudio() { export async function initImagine( region: string, projectId: string, + userId: string, callbacks?: { onProjectNameChange: () => void; onAddDomain: () => void | Promise; @@ -290,6 +295,7 @@ export async function initImagine( }, { initialTheme: get(app).themeInUse, + consoleSessionId: getSessionId(userId), callbacks } ); diff --git a/src/lib/studio/studio.svelte b/src/lib/studio/studio.svelte index 69b891bb5..57c0ecd7c 100644 --- a/src/lib/studio/studio.svelte +++ b/src/lib/studio/studio.svelte @@ -18,10 +18,12 @@ const { region, - projectId + projectId, + userId }: { region: string; projectId: string; + userId: string; } = $props(); const siteId = `project-${projectId}`; @@ -31,7 +33,7 @@ onMount(() => { ensureStudioComponent(); - initImagine(region, projectId, { + initImagine(region, projectId, userId, { onProjectNameChange: () => { invalidate(Dependencies.PROJECT); }, diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index 9a2d98fb2..bc2ca1902 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -333,7 +333,7 @@ }); - + { .then((response) => [response, null]) .catch((error) => [null, error])) as [Account, AppwriteException]; + if (account) identify(account.$id); + if (url.searchParams.has('forceRedirect')) { redirectTo.set(url.searchParams.get('forceRedirect') || null); url.searchParams.delete('forceRedirect');