diff --git a/.gitmodules b/.gitmodules index 41792b62..3be40ab2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "packages/svelte-core"] path = packages/svelte-core - url = git@github.com:OpenShock/svelte-core.git + url = https://github.com/OpenShock/svelte-core.git diff --git a/README.md b/README.md index c4ffeaf7..d5cf06d1 100644 --- a/README.md +++ b/README.md @@ -34,34 +34,15 @@ This is the frontend for the OpenShock project that interfaces with the OpenShoc - `pnpm run dev` - Start the development server - `pnpm run build` - Build the project - `pnpm run preview` - Preview the built project -- `pnpx openapi-generator-cli generate` - Generate the API client +- `pnpm run regen-api` - Generate the API client ## Setup -To get started with development, you will need to have **Node.js** and **pnpm** installed. - -We recommend using a version manager like [Volta](https://docs.volta.sh/guide/getting-started) to manage your Node.js versions. - -### Install Node.js and pnpm - -Install on Linux: - -```bash -curl https://get.volta.sh | bash -``` - -Install on Windows: +To clone the project you may need to rewrite git HTTPS urls to SSH for the submodule checkout to work on your machine. +You can do this with `git config --global url."git@github.com:".insteadOf "https://github.com/"` -```pwsh -winget install Volta.Volta -``` - -Then you can install the required **Node.js** and **pnpm** version by running a single command in the repository root directory: - -```bash -volta install node -volta install pnpm -``` +To get started with development, you will need to have **Node.js** and **pnpm** installed. +We recommend using a node version manager of your choice. ### Install dependencies and build project @@ -154,7 +135,6 @@ src/ # Application source code - `vite.config.ts` — Vite bundler config. - `svelte.config.js` — SvelteKit configuration. - `components.json` — Component metadata for shadcn. -- `openapitools.json` — Codegen config from OpenAPI spec. - `playwright.config.ts` — End-to-end testing configuration (Playwright). ### 🌐 Environment Variables @@ -178,6 +158,11 @@ Any file ending with `.local` are meant for setting local development variables - `Dockerfile` — Container definition for deployment or local dev. - `.dockerignore` — Files to exclude from Docker build context. +## Selfhosting + +A guide to selfhost can be found [on the wiki.](https://wiki.openshock.org/guides/selfhosting) +Alternatively you can deploy it to cloudflare workers using wrangler (or forking it). + ## Support You can support the openshock dev team here: [Sponsor OpenShock](https://github.com/sponsors/OpenShock) diff --git a/package.json b/package.json index 6273475b..7c5ff0bc 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,6 @@ "@internationalized/date": "^3.12.2", "@lucide/svelte": "^1.28.0", "@microsoft/signalr": "^10.0.0", - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/sdk-trace-base": "^2.10.0", "@playwright/test": "^1.62.1", "@sveltejs/adapter-cloudflare": "^7.2.9", "@sveltejs/adapter-node": "^5.5.7", @@ -70,10 +68,12 @@ "vaul-svelte": "1.0.0-next.7", "vite-plugin-devtools-json": "^1.1.0", "vite-plugin-mkcert": "^2.1.0", - "vitest": "^4.1.10" + "vitest": "^4.1.10", + "wrangler": "^4.117.0" }, "dependencies": { "@openshock/svelte-core": "workspace:*", + "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.221.0", "@opentelemetry/exporter-logs-otlp-http": "^0.221.0", "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", @@ -81,6 +81,7 @@ "@opentelemetry/instrumentation-fetch": "^0.221.0", "@opentelemetry/resources": "^2.10.0", "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-trace-node": "^2.10.0", "@opentelemetry/sdk-trace-web": "^2.10.0", "core": "workspace:@openshock/svelte-core@*", "driver.js": "^1.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b8076bd..d8824532 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@openshock/svelte-core': specifier: workspace:* version: link:packages/svelte-core + '@opentelemetry/api': + specifier: ^1.9.1 + version: 1.9.1 '@opentelemetry/api-logs': specifier: ^0.221.0 version: 0.221.0 @@ -46,6 +49,9 @@ importers: '@opentelemetry/sdk-logs': specifier: ^0.221.0 version: 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': + specifier: ^2.10.0 + version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-web': specifier: ^2.10.0 version: 2.10.0(@opentelemetry/api@1.9.1) @@ -80,12 +86,6 @@ importers: '@microsoft/signalr': specifier: ^10.0.0 version: 10.0.0(patch_hash=8117ca356b6ba091260c4f95dce214a6f63cb001019afa4840aea89ae0d8a010) - '@opentelemetry/api': - specifier: ^1.9.1 - version: 1.9.1 - '@opentelemetry/sdk-trace-base': - specifier: ^2.10.0 - version: 2.10.0(@opentelemetry/api@1.9.1) '@playwright/test': specifier: ^1.62.1 version: 1.62.1 @@ -218,6 +218,9 @@ importers: vitest: specifier: ^4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)) + wrangler: + specifier: ^4.117.0 + version: 4.117.0 packages/svelte-core: devDependencies: @@ -961,6 +964,12 @@ packages: resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@opentelemetry/context-async-hooks@2.10.0': + resolution: {integrity: sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.10.0': resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1027,6 +1036,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-node@2.10.0': + resolution: {integrity: sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/sdk-trace-web@2.10.0': resolution: {integrity: sha512-6WqXdWSlBH0GNjDhv1gg6SLVtMtIBVhLaWqd1hiKSXS8meXXP13sVPQoYFHz1uVo0feUTUCdP9vMS54KflisAw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -4297,6 +4312,10 @@ snapshots: '@opentelemetry/api@1.9.1': {} + '@opentelemetry/context-async-hooks@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -4379,6 +4398,13 @@ snapshots: '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/sdk-trace-node@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-web@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 diff --git a/src/instrumentation.server.ts b/src/instrumentation.server.ts index af77339c..d96adc6c 100644 --- a/src/instrumentation.server.ts +++ b/src/instrumentation.server.ts @@ -5,66 +5,15 @@ import { PUBLIC_SIGNOZ_LOGS_URL, PUBLIC_SIGNOZ_TRACES_URL, } from '$env/static/public'; -import { - ROOT_CONTEXT, - context as otelContext, - trace as otelTrace, - type Context, - type ContextManager, -} from '@opentelemetry/api'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { resourceFromAttributes } from '@opentelemetry/resources'; -import { BasicTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { AsyncLocalStorage } from 'node:async_hooks'; +import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; // Collector for SvelteKit's experimental server-side spans (handle/load/form actions, // enabled via kit.experimental.tracing in vite.config.ts). Runs before any application // code is imported, so it must stay self-contained: no $lib imports — the client // telemetry module ($lib/telemetry/common) pulls in consent state and other // browser-oriented code that has no business in the server bootstrap. -// -// Uses BasicTracerProvider + a hand-rolled AsyncLocalStorage context manager instead of -// NodeTracerProvider: sdk-trace-node pulls in @opentelemetry/context-async-hooks, whose -// bare `require("events")`/`require("async_hooks")` Cloudflare's Pages Functions bundler -// cannot resolve. The `node:`-prefixed import below bundles fine and resolves at runtime -// on workerd (nodejs_compat/nodejs_als) and Node alike. - -class AlsContextManager implements ContextManager { - #als = new AsyncLocalStorage(); - - active(): Context { - return this.#als.getStore() ?? ROOT_CONTEXT; - } - - with ReturnType>( - context: Context, - fn: F, - thisArg?: ThisParameterType, - ...args: A - ): ReturnType { - return this.#als.run(context, () => fn.apply(thisArg as ThisParameterType, args)); - } - - bind(context: Context, target: T): T { - if (typeof target !== 'function') return target; - // eslint-disable-next-line @typescript-eslint/no-this-alias -- the wrapper must forward the caller's dynamic `this`, so the manager is captured under another name - const manager = this; - return function (this: unknown, ...args: unknown[]) { - return manager.with(context, () => - (target as (...a: unknown[]) => unknown).apply(this, args) - ); - } as T; - } - - enable(): this { - return this; - } - - disable(): this { - this.#als.disable(); - return this; - } -} const DEFAULT_COLLECTOR_ORIGIN = 'https://signoz.openshock.app'; @@ -78,6 +27,14 @@ function originOf(url: string, fallback: string): string { // Server spans describe our infrastructure, not user behavior, so they are gated only by // the deployment kill-switch — the per-user consent flow in $lib/telemetry does not apply. +// +// PUBLIC_SIGNOZ_LOGS_ENABLED is deliberately one switch for every telemetry signal +// (client logs, client traces, and these server spans) despite the `LOGS` in its name; +// PUBLIC_SIGNOZ_TRACES_URL likewise falls back to the logs collector's origin below. +// +// It is a `$env/static/public` import, so this whole block is dead-code eliminated at +// build time when the flag is off — the default in `.env`. A deploy that wants server +// traces must set it in the build environment, not just at runtime. const enabled = PUBLIC_SIGNOZ_LOGS_ENABLED === 'true' || PUBLIC_SIGNOZ_LOGS_ENABLED === '1'; if (enabled) { @@ -85,7 +42,7 @@ if (enabled) { const collectorOrigin = originOf(PUBLIC_SIGNOZ_LOGS_URL, DEFAULT_COLLECTOR_ORIGIN); const tracesUrl = PUBLIC_SIGNOZ_TRACES_URL || `${collectorOrigin}/v1/traces`; - const provider = new BasicTracerProvider({ + const provider = new NodeTracerProvider({ // Mirrors $lib/telemetry/common buildResource() so client and server signals // correlate as one service in SigNoz; `service.version` (the git SHA) drives // SigNoz deployment markers. @@ -99,8 +56,7 @@ if (enabled) { spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url: tracesUrl }))], }); - otelContext.setGlobalContextManager(new AlsContextManager().enable()); - otelTrace.setGlobalTracerProvider(provider); + provider.register(); } catch (error) { // Tracing is best-effort — a broken collector setup must never take the server down. console.error('Failed to initialize server-side tracing:', error); diff --git a/src/worker-configuration.d.ts b/src/worker-configuration.d.ts new file mode 100644 index 00000000..3fb9b032 --- /dev/null +++ b/src/worker-configuration.d.ts @@ -0,0 +1,35 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --include-runtime=false ./src/worker-configuration.d.ts` (hash: be37b830cd0749e98366116ff4e4d29e) +interface __BaseEnv_Env { + ASSETS: Fetcher; + NODE_PATH: string; + PUBLIC_SITE_NAME: string; + PUBLIC_SITE_DESCRIPTION: string; + PUBLIC_GITHUB_PROJECT_URL: string; + PUBLIC_DISCORD_INVITE_URL: string; + PUBLIC_TURNSTILE_DEV_BYPASS_VALUE: string; + PUBLIC_DEVELOPMENT_BANNER: string; + PUBLIC_DISABLE_ONBOARDING: string; + PUBLIC_DISABLE_SHOCKER_MAP: string; + PUBLIC_SITE_URL: string; + PUBLIC_SITE_SHORT_URL: string; + PUBLIC_BACKEND_API_URL: string; + PUBLIC_GATEWAY_CSP_WILDCARD: string; + PRIVATE_BACKEND_TLS_INSECURE: string; + PUBLIC_SIGNOZ_LOGS_ENABLED: string; + PUBLIC_SIGNOZ_LOGS_URL: string; + PUBLIC_SIGNOZ_TRACES_URL: string; + PUBLIC_SIGNOZ_TRACE_PROPAGATION: string; + PUBLIC_SIGNOZ_DEPLOYMENT_ENVIRONMENT: string; + PUBLIC_SIGNOZ_RESOURCE_ATTRIBUTES: string; +} +declare namespace Cloudflare { + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} +type StringifyValues> = { + [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; +}; +declare namespace NodeJS { + interface ProcessEnv extends StringifyValues> {} +} diff --git a/vite.config.ts b/vite.config.ts index ca5be22d..f38339de 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -58,11 +58,10 @@ const isTruthy = (value?: string) => value === 'true' || value === '1'; // detection, adapter selection, and the CSP in a single, type-checked file. // --------------------------------------------------------------------------- -// Determine if we are running on Cloudflare (Pages git integration or Workers Builds) -const isGithubActions = env.GITHUB_ACTIONS === 'true'; -const isWorkersCI = env.WORKERS_CI === '1'; -const isCloudflare = env.CF_PAGES === '1' || isWorkersCI; -const isDocker = env.DOCKER === 'true'; +// Determine if we are running on Cloudflare Workers Builds +const isGithubActions = isTruthy(env.GITHUB_ACTIONS); +const isCloudflare = isTruthy(env.WORKERS_CI); +const isDocker = isTruthy(env.DOCKER); // Don't trust NODE_ENV — tools like svelte-check load this file mid-process and // can have NODE_ENV='production' set by transitively-imported plugins (vite, // vite-plugin-svelte) even though no production build is actually happening. @@ -76,9 +75,7 @@ const dotenv = { ...env, ...loadEnv(buildMode, process.cwd(), 'PUBLIC_') }; function getGitHash(): string | undefined { if (isGithubActions) return env.GITHUB_SHA; - if (isWorkersCI) return env.WORKERS_CI_COMMIT_SHA; - // Cloudflare Pages prefixes its build vars with CF_PAGES_ - if (isCloudflare) return env.CF_PAGES_COMMIT_SHA; + if (isCloudflare) return env.WORKERS_CI_COMMIT_SHA; if (isDocker) return env.GIT_COMMIT_SHA; return child_process.execSync('git rev-parse HEAD').toString().trim(); @@ -237,7 +234,7 @@ function resolveServerConfig(useLocalRedirect: boolean): LocalServer | undefined export default defineConfig(({ command, mode, isPreview }) => { const isLocalServe = command === 'serve' || isPreview === true; - const isProduction = mode === 'production' && (isTruthy(env.DOCKER) || isTruthy(env.CF_PAGES)); + const isProduction = mode === 'production' && (isTruthy(env.DOCKER) || isTruthy(env.WORKERS_CI)); // Vitest resolves this config with command 'serve', unit tests must never trigger mkcert or the hosts/port checks. const isTest = mode === 'test' || isTruthy(env.VITEST); diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 00000000..727aacd6 --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,59 @@ +/** + * Cloudflare Workers configuration (Workers Builds, WORKERS_CI=1). + * + * Auto-discovered by wrangler and by @sveltejs/adapter-cloudflare, which picks + * Workers output over Pages output because this declares `main`/`assets` and no + * `pages_build_output_dir`. Deploy with a plain `wrangler deploy`. + * + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "openshock-app", + "main": ".svelte-kit/cloudflare/_worker.js", + "compatibility_date": "2026-07-24", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "binding": "ASSETS", + "directory": ".svelte-kit/cloudflare", + }, + "observability": { + "enabled": true, + "head_sampling_rate": 1, + "logs": { + "enabled": true, + "head_sampling_rate": 1, + "persist": true, + "invocation_logs": true, + }, + "traces": { + "enabled": true, + "persist": true, + "head_sampling_rate": 1, + }, + }, + "upload_source_maps": true, + /** + * Smart Placement + * https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + */ + // "placement": { "mode": "smart" } + /** + * Bindings + * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including + * databases, object storage, AI inference, real-time communication and more. + * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + */ + /** + * Environment Variables + * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + * Note: Use secrets to store sensitive data. + * https://developers.cloudflare.com/workers/configuration/secrets/ + */ + /** + * Service Bindings (communicate between multiple Workers) + * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + // "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ] +}