mirror of
https://github.com/documenso/documenso.git
synced 2025-11-26 14:34:05 +10:00
chore: telemetry (#2240)
This commit is contained in:
@@ -141,6 +141,12 @@ NEXT_PUBLIC_DISABLE_SIGNUP=
|
|||||||
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
|
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
|
||||||
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
|
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
|
||||||
|
|
||||||
|
# [[TELEMETRY]]
|
||||||
|
# OPTIONAL: Set to "true" to disable anonymous telemetry for self-hosted instances.
|
||||||
|
# Telemetry helps us understand how Documenso is being used and improve the product.
|
||||||
|
# We only collect: app version, installation ID, and node ID. No personal data is collected.
|
||||||
|
DOCUMENSO_DISABLE_TELEMETRY=
|
||||||
|
|
||||||
# [[E2E Tests]]
|
# [[E2E Tests]]
|
||||||
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
|
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
|
||||||
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
|
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export default {
|
|||||||
'signing-certificate': 'Signing Certificate',
|
'signing-certificate': 'Signing Certificate',
|
||||||
'how-to': 'How To',
|
'how-to': 'How To',
|
||||||
'setting-up-oauth-providers': 'Setting up OAuth Providers',
|
'setting-up-oauth-providers': 'Setting up OAuth Providers',
|
||||||
|
telemetry: 'Telemetry',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -318,6 +318,36 @@ The environment variables listed above are a subset of those available for confi
|
|||||||
| `NEXT_PUBLIC_POSTHOG_KEY` | The optional PostHog key for analytics and feature flags. |
|
| `NEXT_PUBLIC_POSTHOG_KEY` | The optional PostHog key for analytics and feature flags. |
|
||||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Whether to disable user signups through the /signup page. |
|
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Whether to disable user signups through the /signup page. |
|
||||||
| `NEXT_PRIVATE_BROWSERLESS_URL` | The URL for a Browserless.io instance to generate PDFs (optional). |
|
| `NEXT_PRIVATE_BROWSERLESS_URL` | The URL for a Browserless.io instance to generate PDFs (optional). |
|
||||||
|
| `DOCUMENSO_DISABLE_TELEMETRY` | Set to `true` to disable anonymous telemetry (see [Telemetry](#telemetry) section below). |
|
||||||
|
|
||||||
|
## Telemetry
|
||||||
|
|
||||||
|
Documenso collects anonymous telemetry data to help us understand how the software is being used and improve the product. This telemetry is **enabled by default** for self-hosted instances.
|
||||||
|
|
||||||
|
### What We Collect
|
||||||
|
|
||||||
|
We collect minimal, privacy-preserving data:
|
||||||
|
|
||||||
|
- **App Version**: The version of Documenso you are running
|
||||||
|
- **Installation ID**: A unique identifier for your installation (stored in your database)
|
||||||
|
- **Node ID**: A unique identifier for each server/container instance (stored in the OS temp directory)
|
||||||
|
|
||||||
|
We do **not** collect any personal data, document contents, user information, or usage patterns.
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
- **Server Startup**: Captured once when the server starts
|
||||||
|
- **Server Heartbeat**: Captured every hour while the server is running
|
||||||
|
|
||||||
|
### Disabling Telemetry
|
||||||
|
|
||||||
|
To disable telemetry, set the following environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCUMENSO_DISABLE_TELEMETRY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
This will completely disable all telemetry data collection.
|
||||||
|
|
||||||
## Run as a Service
|
## Run as a Service
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Telemetry
|
||||||
|
|
||||||
|
Documenso collects anonymous telemetry data from self-hosted instances to help us understand how the software is being used and make improvements to the product. This telemetry is enabled by default, but you can easily disable it if you prefer.
|
||||||
|
|
||||||
|
## What We Collect
|
||||||
|
|
||||||
|
We collect minimal, privacy-preserving information that helps us understand the health and adoption of self-hosted installations:
|
||||||
|
|
||||||
|
- **App Version**: The version of Documenso you are running. This helps us understand which versions are in use and prioritize support for older versions.
|
||||||
|
|
||||||
|
- **Installation ID**: A unique identifier for your installation. This is stored in your database and helps us count distinct installations without knowing who you are.
|
||||||
|
|
||||||
|
- **Node ID**: A unique identifier for each server or container instance. This is stored in your operating system's temporary directory and helps us understand deployment patterns (for example, how many instances are running in a cluster).
|
||||||
|
|
||||||
|
### What We Don't Collect
|
||||||
|
|
||||||
|
We do **not** collect any of the following:
|
||||||
|
|
||||||
|
- Personal information about you or your users
|
||||||
|
- Document contents or file names
|
||||||
|
- User email addresses or names
|
||||||
|
- Usage patterns or feature usage statistics
|
||||||
|
- Server logs or error messages
|
||||||
|
- Any data that could identify your organization or users
|
||||||
|
|
||||||
|
## Why We Collect Telemetry
|
||||||
|
|
||||||
|
The telemetry data we collect serves several important purposes:
|
||||||
|
|
||||||
|
1. **Product Improvement**: Understanding which versions are in use helps us prioritize bug fixes and security updates for the versions that matter most.
|
||||||
|
|
||||||
|
2. **Support Planning**: Knowing how many installations exist and their deployment patterns helps us plan support resources and documentation.
|
||||||
|
|
||||||
|
3. **Feature Development**: Understanding deployment patterns (like cluster sizes) helps us make better architectural decisions for future features.
|
||||||
|
|
||||||
|
4. **Community Health**: Tracking adoption helps us understand the growth of the self-hosted community and allocate resources accordingly.
|
||||||
|
|
||||||
|
All of this is done anonymously and in aggregate. We cannot identify you, your organization, or your users from the telemetry data we collect.
|
||||||
|
|
||||||
|
## Events We Track
|
||||||
|
|
||||||
|
We track two simple events:
|
||||||
|
|
||||||
|
- **Server Startup**: Captured once when your server starts. This tells us when installations are first set up or restarted.
|
||||||
|
|
||||||
|
- **Server Heartbeat**: Captured every hour while your server is running. This helps us understand how many active installations exist and their uptime patterns.
|
||||||
|
|
||||||
|
## How to Disable Telemetry
|
||||||
|
|
||||||
|
If you prefer not to send telemetry data, you can disable it by setting an environment variable.
|
||||||
|
|
||||||
|
### Using Environment Variables
|
||||||
|
|
||||||
|
Add the following to your environment configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCUMENSO_DISABLE_TELEMETRY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
If you're using Docker, you can set this in your `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
environment:
|
||||||
|
- DOCUMENSO_DISABLE_TELEMETRY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Or pass it when running a container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -e DOCUMENSO_DISABLE_TELEMETRY=true ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### After Disabling
|
||||||
|
|
||||||
|
Once you set `DOCUMENSO_DISABLE_TELEMETRY=true` and restart your server, no telemetry data will be sent. The telemetry client will not initialize, and no network requests will be made to our telemetry servers.
|
||||||
|
|
||||||
|
Note: If you previously had telemetry enabled, the installation ID stored in your database will remain, but it will no longer be used or sent anywhere.
|
||||||
|
|
||||||
|
## Questions or Concerns
|
||||||
|
|
||||||
|
If you have questions about our telemetry practices or concerns about privacy, please reach out to us. We're committed to transparency and respect your choice to disable telemetry if you prefer.
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"posthog-js": "^1.297.2",
|
"posthog-js": "^1.297.2",
|
||||||
"posthog-node": "^4.18.0",
|
"posthog-node": "4.18.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-call": "^1.8.1",
|
"react-call": "^1.8.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
@@ -86,6 +86,7 @@
|
|||||||
"@react-router/remix-routes-option-adapter": "^7.9.6",
|
"@react-router/remix-routes-option-adapter": "^7.9.6",
|
||||||
"@rollup/plugin-babel": "^6.1.0",
|
"@rollup/plugin-babel": "^6.1.0",
|
||||||
"@rollup/plugin-commonjs": "^28.0.9",
|
"@rollup/plugin-commonjs": "^28.0.9",
|
||||||
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
"@rollup/plugin-typescript": "^12.3.0",
|
"@rollup/plugin-typescript": "^12.3.0",
|
||||||
"@simplewebauthn/types": "^9.0.1",
|
"@simplewebauthn/types": "^9.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import linguiMacro from '@lingui/babel-plugin-lingui-macro';
|
import linguiMacro from '@lingui/babel-plugin-lingui-macro';
|
||||||
import babel from '@rollup/plugin-babel';
|
import babel from '@rollup/plugin-babel';
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import json from '@rollup/plugin-json';
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
import typescript from '@rollup/plugin-typescript';
|
import typescript from '@rollup/plugin-typescript';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@@ -39,6 +40,7 @@ const config = {
|
|||||||
],
|
],
|
||||||
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
|
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
|
||||||
}),
|
}),
|
||||||
|
json(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
babel({
|
babel({
|
||||||
babelHelpers: 'bundled',
|
babelHelpers: 'bundled',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { tsRestHonoApp } from '@documenso/api/hono';
|
|||||||
import { auth } from '@documenso/auth/server';
|
import { auth } from '@documenso/auth/server';
|
||||||
import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app';
|
import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app';
|
||||||
import { jobsClient } from '@documenso/lib/jobs/client';
|
import { jobsClient } from '@documenso/lib/jobs/client';
|
||||||
|
import { TelemetryClient } from '@documenso/lib/server-only/telemetry/telemetry-client';
|
||||||
import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
|
import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
|
||||||
import { logger } from '@documenso/lib/utils/logger';
|
import { logger } from '@documenso/lib/utils/logger';
|
||||||
import { openApiDocument } from '@documenso/trpc/server/open-api';
|
import { openApiDocument } from '@documenso/trpc/server/open-api';
|
||||||
@@ -112,4 +113,8 @@ app.use(`${API_V2_BETA_URL}/*`, async (c) =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start telemetry client for anonymous usage tracking.
|
||||||
|
// Can be disabled by setting DOCUMENSO_DISABLE_TELEMETRY=true
|
||||||
|
void TelemetryClient.start();
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ ENV NEXT_PRIVATE_ENCRYPTION_KEY="$NEXT_PRIVATE_ENCRYPTION_KEY"
|
|||||||
ARG NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
|
ARG NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
|
||||||
ENV NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="$NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY"
|
ENV NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="$NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY"
|
||||||
|
|
||||||
|
# Telemetry credentials (optional, baked into image at build time)
|
||||||
|
ARG NEXT_PRIVATE_TELEMETRY_KEY=""
|
||||||
|
ENV NEXT_PRIVATE_TELEMETRY_KEY="$NEXT_PRIVATE_TELEMETRY_KEY"
|
||||||
|
|
||||||
|
ARG NEXT_PRIVATE_TELEMETRY_HOST=""
|
||||||
|
ENV NEXT_PRIVATE_TELEMETRY_HOST="$NEXT_PRIVATE_TELEMETRY_HOST"
|
||||||
|
|
||||||
|
|
||||||
# Uncomment and use build args to enable remote caching
|
# Uncomment and use build args to enable remote caching
|
||||||
# ARG TURBO_TEAM
|
# ARG TURBO_TEAM
|
||||||
@@ -83,6 +90,13 @@ FROM base AS runner
|
|||||||
ENV HUSKY 0
|
ENV HUSKY 0
|
||||||
ENV DOCKER_OUTPUT 1
|
ENV DOCKER_OUTPUT 1
|
||||||
|
|
||||||
|
# Telemetry credentials (baked into image at build time, can be disabled at runtime)
|
||||||
|
ARG NEXT_PRIVATE_TELEMETRY_KEY=""
|
||||||
|
ENV NEXT_PRIVATE_TELEMETRY_KEY="$NEXT_PRIVATE_TELEMETRY_KEY"
|
||||||
|
|
||||||
|
ARG NEXT_PRIVATE_TELEMETRY_HOST=""
|
||||||
|
ENV NEXT_PRIVATE_TELEMETRY_HOST="$NEXT_PRIVATE_TELEMETRY_HOST"
|
||||||
|
|
||||||
# Don't run production as root
|
# Don't run production as root
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nodejs
|
RUN adduser --system --uid 1001 nodejs
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ echo "Git SHA: $GIT_SHA"
|
|||||||
# Build with temporary base tag
|
# Build with temporary base tag
|
||||||
docker build -f "$SCRIPT_DIR/Dockerfile" \
|
docker build -f "$SCRIPT_DIR/Dockerfile" \
|
||||||
--progress=plain \
|
--progress=plain \
|
||||||
|
--build-arg NEXT_PRIVATE_TELEMETRY_KEY="${NEXT_PRIVATE_TELEMETRY_KEY:-}" \
|
||||||
|
--build-arg NEXT_PRIVATE_TELEMETRY_HOST="${NEXT_PRIVATE_TELEMETRY_HOST:-}" \
|
||||||
-t "documenso-base" \
|
-t "documenso-base" \
|
||||||
"$MONOREPO_ROOT"
|
"$MONOREPO_ROOT"
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ docker buildx build \
|
|||||||
-f "$SCRIPT_DIR/Dockerfile" \
|
-f "$SCRIPT_DIR/Dockerfile" \
|
||||||
--platform=$PLATFORM \
|
--platform=$PLATFORM \
|
||||||
--progress=plain \
|
--progress=plain \
|
||||||
|
--build-arg NEXT_PRIVATE_TELEMETRY_KEY="${NEXT_PRIVATE_TELEMETRY_KEY:-}" \
|
||||||
|
--build-arg NEXT_PRIVATE_TELEMETRY_HOST="${NEXT_PRIVATE_TELEMETRY_HOST:-}" \
|
||||||
-t "documenso/documenso:latest" \
|
-t "documenso/documenso:latest" \
|
||||||
-t "documenso/documenso:$GIT_SHA" \
|
-t "documenso/documenso:$GIT_SHA" \
|
||||||
-t "documenso/documenso:$APP_VERSION" \
|
-t "documenso/documenso:$APP_VERSION" \
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ docker buildx build \
|
|||||||
-f "$SCRIPT_DIR/Dockerfile" \
|
-f "$SCRIPT_DIR/Dockerfile" \
|
||||||
--platform=$PLATFORM \
|
--platform=$PLATFORM \
|
||||||
--progress=plain \
|
--progress=plain \
|
||||||
|
--build-arg NEXT_PRIVATE_TELEMETRY_KEY="${NEXT_PRIVATE_TELEMETRY_KEY:-}" \
|
||||||
|
--build-arg NEXT_PRIVATE_TELEMETRY_HOST="${NEXT_PRIVATE_TELEMETRY_HOST:-}" \
|
||||||
-t "documenso-base" \
|
-t "documenso-base" \
|
||||||
"$MONOREPO_ROOT"
|
"$MONOREPO_ROOT"
|
||||||
|
|
||||||
|
|||||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"@lingui/core": "^5.6.0",
|
"@lingui/core": "^5.6.0",
|
||||||
"inngest-cli": "^1.13.7",
|
"inngest-cli": "^1.13.7",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
|
"posthog-node": "4.18.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -148,7 +149,7 @@
|
|||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"posthog-js": "^1.297.2",
|
"posthog-js": "^1.297.2",
|
||||||
"posthog-node": "^4.18.0",
|
"posthog-node": "4.18.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-call": "^1.8.1",
|
"react-call": "^1.8.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
@@ -178,6 +179,7 @@
|
|||||||
"@react-router/remix-routes-option-adapter": "^7.9.6",
|
"@react-router/remix-routes-option-adapter": "^7.9.6",
|
||||||
"@rollup/plugin-babel": "^6.1.0",
|
"@rollup/plugin-babel": "^6.1.0",
|
||||||
"@rollup/plugin-commonjs": "^28.0.9",
|
"@rollup/plugin-commonjs": "^28.0.9",
|
||||||
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
"@rollup/plugin-typescript": "^12.3.0",
|
"@rollup/plugin-typescript": "^12.3.0",
|
||||||
"@simplewebauthn/types": "^9.0.1",
|
"@simplewebauthn/types": "^9.0.1",
|
||||||
@@ -14695,6 +14697,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/plugin-json": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/plugin-node-resolve": {
|
"node_modules/@rollup/plugin-node-resolve": {
|
||||||
"version": "16.0.3",
|
"version": "16.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
|
||||||
@@ -30810,6 +30833,7 @@
|
|||||||
"version": "4.18.0",
|
"version": "4.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.18.0.tgz",
|
||||||
"integrity": "sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==",
|
"integrity": "sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.2"
|
"axios": "^1.8.2"
|
||||||
@@ -37081,7 +37105,7 @@
|
|||||||
"pino-pretty": "^13.1.2",
|
"pino-pretty": "^13.1.2",
|
||||||
"playwright": "1.56.1",
|
"playwright": "1.56.1",
|
||||||
"posthog-js": "^1.297.2",
|
"posthog-js": "^1.297.2",
|
||||||
"posthog-node": "^4.18.0",
|
"posthog-node": "4.18.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-pdf": "^10.2.0",
|
"react-pdf": "^10.2.0",
|
||||||
"remeda": "^2.32.0",
|
"remeda": "^2.32.0",
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
"@lingui/core": "^5.6.0",
|
"@lingui/core": "^5.6.0",
|
||||||
"inngest-cli": "^1.13.7",
|
"inngest-cli": "^1.13.7",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
|
"posthog-node": "4.18.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
"pino-pretty": "^13.1.2",
|
"pino-pretty": "^13.1.2",
|
||||||
"playwright": "1.56.1",
|
"playwright": "1.56.1",
|
||||||
"posthog-js": "^1.297.2",
|
"posthog-js": "^1.297.2",
|
||||||
"posthog-node": "^4.18.0",
|
"posthog-node": "4.18.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-pdf": "^10.2.0",
|
"react-pdf": "^10.2.0",
|
||||||
"remeda": "^2.32.0",
|
"remeda": "^2.32.0",
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { PostHog } from 'posthog-node';
|
|
||||||
|
|
||||||
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
|
|
||||||
|
|
||||||
export default function PostHogServerClient() {
|
|
||||||
const postHogConfig = extractPostHogConfig();
|
|
||||||
|
|
||||||
if (!postHogConfig) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PostHog(postHogConfig.key, {
|
|
||||||
host: postHogConfig.host,
|
|
||||||
fetch: async (...args) => fetch(...args),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
22
packages/lib/server-only/site-settings/get-site-setting.ts
Normal file
22
packages/lib/server-only/site-settings/get-site-setting.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import type { TSiteSettingSchema } from './schema';
|
||||||
|
import { ZSiteSettingSchema } from './schema';
|
||||||
|
|
||||||
|
export const getSiteSetting = async <
|
||||||
|
T extends TSiteSettingSchema['id'],
|
||||||
|
U = Extract<TSiteSettingSchema, { id: T }>,
|
||||||
|
>(options: {
|
||||||
|
id: T;
|
||||||
|
}): Promise<U> => {
|
||||||
|
const { id } = options;
|
||||||
|
|
||||||
|
const setting = await prisma.siteSettings.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return ZSiteSettingSchema.parse(setting) as U;
|
||||||
|
};
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZSiteSettingsBannerSchema } from './schemas/banner';
|
import { ZSiteSettingsBannerSchema } from './schemas/banner';
|
||||||
|
import { ZSiteSettingsTelemetrySchema } from './schemas/telemetry';
|
||||||
|
|
||||||
// TODO: Use `z.union([...])` once we have more than one setting
|
export const ZSiteSettingSchema = z.union([
|
||||||
export const ZSiteSettingSchema = ZSiteSettingsBannerSchema;
|
ZSiteSettingsBannerSchema,
|
||||||
|
ZSiteSettingsTelemetrySchema,
|
||||||
|
]);
|
||||||
|
|
||||||
export type TSiteSettingSchema = z.infer<typeof ZSiteSettingSchema>;
|
export type TSiteSettingSchema = z.infer<typeof ZSiteSettingSchema>;
|
||||||
|
|
||||||
|
|||||||
14
packages/lib/server-only/site-settings/schemas/telemetry.ts
Normal file
14
packages/lib/server-only/site-settings/schemas/telemetry.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ZSiteSettingsBaseSchema } from './_base';
|
||||||
|
|
||||||
|
export const SITE_SETTINGS_TELEMETRY_ID = 'telemetry.installation';
|
||||||
|
|
||||||
|
export const ZSiteSettingsTelemetrySchema = ZSiteSettingsBaseSchema.extend({
|
||||||
|
id: z.literal(SITE_SETTINGS_TELEMETRY_ID),
|
||||||
|
data: z.object({
|
||||||
|
installationId: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSiteSettingsTelemetrySchema = z.infer<typeof ZSiteSettingsTelemetrySchema>;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import type { TSiteSettingSchema } from './schema';
|
import { type TSiteSettingSchema } from './schema';
|
||||||
|
|
||||||
export type UpsertSiteSettingOptions = TSiteSettingSchema & {
|
export type UpsertSiteSettingOptions = TSiteSettingSchema & {
|
||||||
userId: number;
|
userId?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertSiteSetting = async ({
|
export const upsertSiteSetting = async ({
|
||||||
|
|||||||
190
packages/lib/server-only/telemetry/telemetry-client.ts
Normal file
190
packages/lib/server-only/telemetry/telemetry-client.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/* eslint-disable require-atomic-updates */
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { PostHog } from 'posthog-node';
|
||||||
|
|
||||||
|
import { version } from '../../../../package.json';
|
||||||
|
import { prefixedId } from '../../universal/id';
|
||||||
|
import { getSiteSetting } from '../site-settings/get-site-setting';
|
||||||
|
import { SITE_SETTINGS_TELEMETRY_ID } from '../site-settings/schemas/telemetry';
|
||||||
|
import { upsertSiteSetting } from '../site-settings/upsert-site-setting';
|
||||||
|
|
||||||
|
const TELEMETRY_KEY = process.env.NEXT_PRIVATE_TELEMETRY_KEY;
|
||||||
|
const TELEMETRY_HOST = process.env.NEXT_PRIVATE_TELEMETRY_HOST;
|
||||||
|
const TELEMETRY_DISABLED = !!process.env.DOCUMENSO_DISABLE_TELEMETRY;
|
||||||
|
|
||||||
|
const NODE_ID_FILENAME = '.documenso-node-id';
|
||||||
|
const HEARTBEAT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
|
// Version is hardcoded to avoid rollup JSON import issues
|
||||||
|
const APP_VERSION = version;
|
||||||
|
|
||||||
|
export class TelemetryClient {
|
||||||
|
private static instance: TelemetryClient | null = null;
|
||||||
|
|
||||||
|
private client: PostHog | null = null;
|
||||||
|
|
||||||
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
private installationId: string | null = null;
|
||||||
|
private nodeId: string | null = null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the telemetry client.
|
||||||
|
*
|
||||||
|
* This will initialize the PostHog client, load or create the installation ID and node ID,
|
||||||
|
* capture a startup event, and start a heartbeat interval.
|
||||||
|
*
|
||||||
|
* If telemetry is disabled via `DOCUMENSO_DISABLE_TELEMETRY=true` or credentials are not
|
||||||
|
* provided, this will be a no-op.
|
||||||
|
*/
|
||||||
|
public static async start(): Promise<void> {
|
||||||
|
if (TELEMETRY_DISABLED) {
|
||||||
|
console.log(
|
||||||
|
'[Telemetry] Telemetry is disabled. To enable, remove the DOCUMENSO_DISABLE_TELEMETRY environment variable.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TELEMETRY_KEY || !TELEMETRY_HOST) {
|
||||||
|
console.log('[Telemetry] Telemetry credentials not configured. Telemetry will not be sent.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TelemetryClient.instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new TelemetryClient();
|
||||||
|
|
||||||
|
TelemetryClient.instance = instance;
|
||||||
|
|
||||||
|
await instance.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the telemetry client.
|
||||||
|
*
|
||||||
|
* This will clear the heartbeat interval and shutdown the PostHog client.
|
||||||
|
*/
|
||||||
|
public static async stop(): Promise<void> {
|
||||||
|
const instance = TelemetryClient.instance;
|
||||||
|
|
||||||
|
if (!instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance.heartbeatInterval) {
|
||||||
|
clearInterval(instance.heartbeatInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance.client) {
|
||||||
|
await instance.client.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TelemetryClient.instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
this.client = new PostHog(TELEMETRY_KEY!, {
|
||||||
|
host: TELEMETRY_HOST,
|
||||||
|
disableGeoip: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load or create IDs
|
||||||
|
this.installationId = await this.getOrCreateInstallationId();
|
||||||
|
this.nodeId = await this.getOrCreateNodeId();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[Telemetry] Telemetry is enabled. Documenso collects anonymous usage data to help improve the product.',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'[Telemetry] We collect: app version, installation ID, and node ID. No personal data, document contents, or user information is collected.',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'[Telemetry] To disable telemetry, set DOCUMENSO_DISABLE_TELEMETRY=true in your environment variables.',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'[Telemetry] Learn more: https://documenso.com/docs/developers/self-hosting/telemetry',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Capture startup event
|
||||||
|
this.captureEvent('telemetry_selfhoster_startup');
|
||||||
|
|
||||||
|
// Start heartbeat
|
||||||
|
this.heartbeatInterval = setInterval(() => {
|
||||||
|
this.captureEvent('telemetry_selfhoster_heartbeat');
|
||||||
|
}, HEARTBEAT_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private captureEvent(event: string): void {
|
||||||
|
if (!this.client || !this.installationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.capture({
|
||||||
|
distinctId: this.installationId,
|
||||||
|
event,
|
||||||
|
properties: {
|
||||||
|
appVersion: APP_VERSION,
|
||||||
|
installationId: this.installationId,
|
||||||
|
nodeId: this.nodeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreateInstallationId(): Promise<string> {
|
||||||
|
try {
|
||||||
|
// Try to get from site settings
|
||||||
|
const existing = await getSiteSetting({ id: SITE_SETTINGS_TELEMETRY_ID }).catch(() => null);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.data.installationId) {
|
||||||
|
return existing.data.installationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new installation ID
|
||||||
|
const installationId = prefixedId('installation');
|
||||||
|
|
||||||
|
await upsertSiteSetting({
|
||||||
|
id: SITE_SETTINGS_TELEMETRY_ID,
|
||||||
|
data: { installationId },
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return installationId;
|
||||||
|
} catch {
|
||||||
|
// If database is not available, generate a temporary ID
|
||||||
|
return prefixedId('installation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreateNodeId(): Promise<string | null> {
|
||||||
|
const nodeIdPath = path.join(os.tmpdir(), NODE_ID_FILENAME);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingId = await fs.readFile(nodeIdPath, 'utf-8');
|
||||||
|
|
||||||
|
if (existingId.trim()) {
|
||||||
|
return existingId.trim();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist or can't be read, continue to create
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new node ID
|
||||||
|
const nodeId = prefixedId('node');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(nodeIdPath, nodeId, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
// Read-only filesystem, use memory for nodeId
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,18 +10,16 @@ export const updateSiteSettingRoute = adminProcedure
|
|||||||
.input(ZUpdateSiteSettingRequestSchema)
|
.input(ZUpdateSiteSettingRequestSchema)
|
||||||
.output(ZUpdateSiteSettingResponseSchema)
|
.output(ZUpdateSiteSettingResponseSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { id, enabled, data } = input;
|
const { ...siteSetting } = input;
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
input: {
|
input: {
|
||||||
id,
|
id: siteSetting.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await upsertSiteSetting({
|
await upsertSiteSetting({
|
||||||
id,
|
...siteSetting,
|
||||||
enabled,
|
|
||||||
data,
|
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,6 +109,9 @@
|
|||||||
"NEXT_PRIVATE_INNGEST_APP_ID",
|
"NEXT_PRIVATE_INNGEST_APP_ID",
|
||||||
"INNGEST_EVENT_KEY",
|
"INNGEST_EVENT_KEY",
|
||||||
"NEXT_PRIVATE_INNGEST_EVENT_KEY",
|
"NEXT_PRIVATE_INNGEST_EVENT_KEY",
|
||||||
|
"NEXT_PRIVATE_TELEMETRY_KEY",
|
||||||
|
"NEXT_PRIVATE_TELEMETRY_HOST",
|
||||||
|
"DOCUMENSO_DISABLE_TELEMETRY",
|
||||||
"CI",
|
"CI",
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
"POSTGRES_URL",
|
"POSTGRES_URL",
|
||||||
|
|||||||
Reference in New Issue
Block a user