Self-hosting! (#49)

* added self-hosting
This commit is contained in:
Carl-Gerhard Lindesvärd
2024-08-28 09:28:44 +02:00
committed by GitHub
parent f0b7526847
commit df05e2dab3
70 changed files with 2310 additions and 272 deletions

View File

@@ -1,86 +1,95 @@
# Dockerfile that builds the web app only
ARG NODE_VERSION=20.15.1
FROM --platform=linux/amd64 node:${NODE_VERSION}-slim AS base
FROM node:${NODE_VERSION}-slim AS base
RUN corepack enable && apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
openssl \
libssl3 \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN apt update \
&& apt install -y curl python3 make g++ \
&& curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \
&& bash n $NODE_VERSION \
&& rm n \
&& npm install -g n \
&& rm -rf /var/cache/apk/*
WORKDIR /app
COPY package.json package.json
COPY pnpm-lock.yaml pnpm-lock.yaml
COPY pnpm-workspace.yaml pnpm-workspace.yaml
COPY apps/api/package.json apps/api/package.json
COPY packages/db/package.json packages/db/package.json
COPY packages/redis/package.json packages/redis/package.json
COPY packages/trpc/package.json packages/trpc/package.json
COPY packages/queue/package.json packages/queue/package.json
COPY packages/common/package.json packages/common/package.json
COPY packages/constants/package.json packages/constants/package.json
COPY packages/validation/package.json packages/validation/package.json
COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json
# Workspace
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# Apps
COPY apps/api/package.json ./apps/api/
# Packages
COPY packages/db/package.json packages/db/
COPY packages/trpc/package.json packages/trpc/
COPY packages/queue/package.json packages/queue/
COPY packages/redis/package.json packages/redis/
COPY packages/common/package.json packages/common/
COPY packages/sdks/sdk/package.json packages/sdks/sdk/
COPY packages/constants/package.json packages/constants/
COPY packages/validation/package.json packages/validation/
COPY packages/sdks/sdk/package.json packages/sdks/sdk/
# Patches
COPY patches patches
# BUILD
FROM base AS build
WORKDIR /app/apps/api
RUN pnpm install --frozen-lockfile
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
g++
WORKDIR /app
COPY apps/api apps/api
COPY packages packages
COPY tooling tooling
RUN pnpm db:codegen
WORKDIR /app
RUN pnpm install --frozen-lockfile && \
pnpm store prune
WORKDIR /app/apps/api
RUN pnpm run build
COPY apps/api ./apps/api
COPY packages ./packages
COPY tooling ./tooling
RUN pnpm db:codegen && \
pnpm --filter api run build
# PROD
FROM base AS prod
WORKDIR /app/apps/api
RUN pnpm install --frozen-lockfile --prod
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
g++
WORKDIR /app
COPY --from=build /app/package.json ./
COPY --from=build /app/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod && \
pnpm store prune
# FINAL
FROM base AS runner
COPY --from=build /app/package.json /app/package.json
COPY --from=prod /app/node_modules /app/node_modules
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/package.json ./
COPY --from=prod /app/node_modules ./node_modules
# Apps
COPY --from=build /app/apps/api /app/apps/api
# Apps node_modules
COPY --from=prod /app/apps/api/node_modules /app/apps/api/node_modules
COPY --from=build /app/apps/api ./apps/api
# Packages
COPY --from=build /app/packages/db /app/packages/db
COPY --from=build /app/packages/redis /app/packages/redis
COPY --from=build /app/packages/trpc /app/packages/trpc
COPY --from=build /app/packages/queue /app/packages/queue
COPY --from=build /app/packages/common /app/packages/common
# Packages node_modules
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules
COPY --from=prod /app/packages/trpc/node_modules /app/packages/trpc/node_modules
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules
COPY --from=build /app/packages/db ./packages/db
COPY --from=build /app/packages/trpc ./packages/trpc
COPY --from=build /app/packages/queue ./packages/queue
COPY --from=build /app/packages/redis ./packages/redis
COPY --from=build /app/packages/common ./packages/common
COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk
COPY --from=build /app/packages/constants ./packages/constants
COPY --from=build /app/packages/validation ./packages/validation
RUN pnpm db:codegen

View File

@@ -5,6 +5,7 @@ import icoToPng from 'ico-to-png';
import sharp from 'sharp';
import { createHash } from '@openpanel/common';
import { ch, TABLE_NAMES } from '@openpanel/db';
import { getRedisCache } from '@openpanel/redis';
interface GetFaviconParams {
@@ -110,3 +111,37 @@ export async function clearFavicons(
}
return reply.status(404).send('OK');
}
export async function ping(
request: FastifyRequest<{
Body: {
domain: string;
count: number;
};
}>,
reply: FastifyReply
) {
try {
await ch.insert({
table: TABLE_NAMES.self_hosting,
values: [
{
domain: request.body.domain,
count: request.body.count,
created_at: new Date(),
},
],
format: 'JSONEachRow',
});
reply.status(200).send({
message: `Success`,
count: request.body.count,
domain: request.body.domain,
});
} catch (e) {
logger.error(e, 'Failed to insert ping');
reply.status(500).send({
error: 'Failed to insert ping',
});
}
}

View File

@@ -44,6 +44,7 @@ export async function clerkWebhook(
if (payload.type === 'user.created') {
const email = payload.data.email_addresses[0]?.email_address;
const emails = payload.data.email_addresses.map((e) => e.email_address);
if (!email) {
return Response.json(
@@ -63,14 +64,16 @@ export async function clerkWebhook(
const memberships = await db.member.findMany({
where: {
email,
email: {
in: emails,
},
userId: null,
},
});
for (const membership of memberships) {
const access = pathOr<string[]>([], ['meta', 'access'], membership);
db.$transaction([
await db.$transaction([
// Update the member to link it to the user
// This will remove the item from invitations
db.member.update({
@@ -123,7 +126,6 @@ export async function clerkWebhook(
deletedAt: new Date(),
firstName: null,
lastName: null,
email: `deleted+${payload.data.id}@openpanel.dev`,
},
}),
db.projectAccess.deleteMany({

View File

@@ -2,6 +2,12 @@ import * as controller from '@/controllers/misc.controller';
import type { FastifyPluginCallback } from 'fastify';
const miscRouter: FastifyPluginCallback = (fastify, opts, done) => {
fastify.route({
method: 'POST',
url: '/ping',
handler: controller.ping,
});
fastify.route({
method: 'GET',
url: '/favicon',

View File

@@ -1,6 +1,6 @@
ARG NODE_VERSION=20.15.1
FROM --platform=linux/amd64 node:${NODE_VERSION}-slim AS base
FROM node:${NODE_VERSION}-slim AS base
ENV SKIP_ENV_VALIDATION="1"
@@ -11,17 +11,15 @@ ARG ENABLE_INSTRUMENTATION_HOOK
ENV ENABLE_INSTRUMENTATION_HOOK=$ENABLE_INSTRUMENTATION_HOOK
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# Install necessary dependencies for prisma
RUN apt-get update && apt-get install -y \
openssl \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
RUN apt update \
&& apt install -y curl \
&& curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \
&& bash n $NODE_VERSION \
&& rm n \
&& npm install -g n
RUN corepack enable
WORKDIR /app
@@ -39,14 +37,14 @@ COPY packages/common/package.json packages/common/package.json
COPY packages/constants/package.json packages/constants/package.json
COPY packages/validation/package.json packages/validation/package.json
COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json
COPY patches patches
# BUILD
FROM base AS build
WORKDIR /app/apps/dashboard
WORKDIR /app
RUN pnpm install --frozen-lockfile --ignore-scripts
WORKDIR /app
COPY apps/dashboard apps/dashboard
COPY packages packages
COPY tooling tooling
@@ -57,48 +55,44 @@ WORKDIR /app/apps/dashboard
# Will be replaced on runtime
ENV NEXT_PUBLIC_DASHBOARD_URL="__NEXT_PUBLIC_DASHBOARD_URL__"
ENV NEXT_PUBLIC_API_URL="__NEXT_PUBLIC_API_URL__"
# Check entrypoint for this little fellow
ENV NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_eW9sby5jb20k"
# Does not need to be replaced
ENV NEXT_PUBLIC_CLERK_SIGN_IN_URL="/login"
ENV NEXT_PUBLIC_CLERK_SIGN_UP_URL="/register"
ENV NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/"
ENV NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/"
RUN pnpm run build
# PROD
FROM base AS prod
WORKDIR /app/apps/dashboard
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
# FINAL
# RUNNER
FROM base AS runner
COPY --from=build /app/package.json /app/package.json
COPY --from=prod /app/node_modules /app/node_modules
# Apps
COPY --from=build /app/apps/dashboard /app/apps/dashboard
# Apps node_modules
COPY --from=prod /app/apps/dashboard/node_modules /app/apps/dashboard/node_modules
# Packages
COPY --from=build /app/packages/db /app/packages/db
COPY --from=build /app/packages/redis /app/packages/redis
COPY --from=build /app/packages/common /app/packages/common
COPY --from=build /app/packages/queue /app/packages/queue
COPY --from=build /app/packages/constants /app/packages/constants
COPY --from=build /app/packages/validation /app/packages/validation
COPY --from=build /app/packages/sdks/sdk /app/packages/sdks/sdk
# Packages node_modules
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules
COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules
COPY --from=prod /app/packages/validation/node_modules /app/packages/validation/node_modules
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
WORKDIR /app
RUN pnpm db:codegen
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
WORKDIR /app/apps/dashboard
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Set the correct permissions for the entire /app directory
COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/.next/static ./apps/dashboard/.next/static
COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/public ./apps/dashboard/public
# Copy and set permissions for the entrypoint script
COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
USER nextjs
EXPOSE 3000
# CMD ["pnpm", "start"]
COPY --from=build /app/apps/dashboard/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh", "pnpm", "start"]
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
ENTRYPOINT [ "/app/entrypoint.sh", "node", "/app/apps/dashboard/server.js"]

View File

@@ -1,32 +1,39 @@
#!/bin/bash
#!/bin/sh
set -e
echo "> Replace env variable placeholders with runtime values..."
# Define an array of environment variables to check
variables_to_replace=(
"NEXT_PUBLIC_DASHBOARD_URL"
"NEXT_PUBLIC_API_URL"
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
)
# Define environment variables to check (space-separated string)
variables_to_replace="NEXT_PUBLIC_DASHBOARD_URL NEXT_PUBLIC_API_URL NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
# Replace env variable placeholders with real values
for key in "${variables_to_replace[@]}"; do
value=$(printenv $key)
if [ ! -z "$value" ]; then
echo " - Searching for $key with value $value..."
# Use a custom placeholder for 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY' or use the actual key otherwise
if [ "$key" = "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" ]; then
placeholder="pk_test_eW9sby5jb20k"
for key in $variables_to_replace; do
value=$(eval echo \$"$key")
if [ -n "$value" ]; then
echo " - Searching for $key with value $value..."
# Use a custom placeholder for 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY' or use the actual key otherwise
case "$key" in
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY)
placeholder="pk_test_eW9sby5jb20k"
;;
*)
placeholder="__${key}__"
;;
esac
# Run the replacement
find /app -type f \( -name "*.js" -o -name "*.html" \) | while read -r file; do
if grep -q "$placeholder" "$file"; then
echo " - Replacing in file: $file"
sed -i "s|$placeholder|$value|g" "$file"
fi
done
else
placeholder="__${key}__"
echo " - Skipping $key as it has no value set."
fi
# Run the replacement
find /app/apps/dashboard/.next/ -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i "s|$placeholder|$value|g" {} \;
else
echo " - Skipping $key as it has no value set."
fi
done
echo "> Done!"
echo "> Running $@"
# Execute the container's main process (CMD in Dockerfile)
exec "$@"

View File

@@ -9,6 +9,7 @@ await import('./src/env.mjs');
/** @type {import("next").NextConfig} */
const config = {
output: 'standalone',
webpack: (config, { isServer }) => {
if (isServer) {
config.plugins = [...config.plugins, new PrismaPlugin()];

View File

@@ -5,7 +5,7 @@
"scripts": {
"dev": "rm -rf .next && pnpm with-env next dev",
"testing": "pnpm dev",
"build": "next build",
"build": "pnpm with-env next build",
"start": "next start",
"lint": "eslint .",
"format": "prettier --check \"**/*.{tsx,mjs,ts,md,json}\"",

View File

@@ -16,9 +16,8 @@ const SkipOnboarding = () => {
res.refetch();
}, [pathname]);
console.log(res.data);
if (!pathname.startsWith('/onboarding')) return null;
return (
<button
onClick={() => {
@@ -30,6 +29,7 @@ const SkipOnboarding = () => {
text: 'Are you sure you want to skip onboarding? Since you do not have any projects, you will be logged out.',
onConfirm() {
auth.signOut();
router.replace(process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL);
},
});
}

View File

@@ -2,6 +2,7 @@ import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
id: process.env.NEXT_PUBLIC_DASHBOARD_URL,
name: 'Openpanel.dev',
short_name: 'Openpanel.dev',
description: '',
@@ -9,12 +10,5 @@ export default function manifest(): MetadataRoute.Manifest {
display: 'standalone',
background_color: '#fff',
theme_color: '#fff',
icons: [
{
src: '/favicon.ico',
sizes: 'any',
type: 'image/x-icon',
},
],
};
}

View File

@@ -182,7 +182,7 @@ $openpanel->event(
<strong>Usage</strong>
<p>Create a custom event called &quot;my_event&quot;.</p>
<Syntax
code={`curl 'https://api.openpanel.dev/track' \\
code={`curl '${process.env.NEXT_PUBLIC_API_URL}/track' \\
-H 'content-type: application/json' \\
-H 'openpanel-client-id: ${clientId}' \\
-H 'openpanel-client-secret: ${clientSecret}' \\

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 424 KiB

View File

@@ -1,10 +1,10 @@
{
"index": {
"title": "Introduction",
"title": "Home",
"type": "page"
},
"docs": {
"title": "Documentation",
"type": "page"
}
}
}

View File

@@ -1,25 +1,6 @@
{
"index": "Get Started",
"-- Frameworks": {
"type": "separator",
"title": "Frameworks"
},
"script": "Web (Script Tag)",
"web": "Web (Module)",
"nextjs": "Next.js",
"react": "React",
"react-native": "React-Native",
"remix": "Remix",
"vue": "Vue",
"astro": "Astro",
"node": "Node",
"express": "Express (backend)",
"-- Others": {
"type": "separator",
"title": "Others"
},
"javascript": "JavaScript",
"api": "API",
"export": "Export",
"migration": "Migrations"
"sdks": "SDKs",
"migration": "Migrations",
"self-hosting": "Self-hosting"
}

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# Astro
You can use <Link href="/docs/script">script tag</Link> or <Link href="/docs/web">Web SDK</Link> to track events in Astro.

View File

@@ -68,7 +68,7 @@ Clears the current user identifier and ends the session.
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/240px-HTML5_logo_and_wordmark.svg.png" />
}
title="HTML / Script"
href="/docs/script"
href="/docs/sdks/script"
>
{' '}
</Card>
@@ -77,7 +77,7 @@ Clears the current user identifier and ends the session.
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png" />
}
title="React"
href="/docs/react"
href="/docs/sdks/react"
>
{' '}
</Card>
@@ -86,7 +86,7 @@ Clears the current user identifier and ends the session.
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png" />
}
title="React-Native"
href="/docs/react-native"
href="/docs/sdks/react-native"
>
{' '}
</Card>
@@ -94,11 +94,11 @@ Clears the current user identifier and ends the session.
icon={
<BrandLogo
isDark
src="https://static-00.iconduck.com/assets.00/nextjs-icon-512x512-y563b8iq.png"
src="https://pbs.twimg.com/profile_images/1565710214019444737/if82cpbS_400x400.jpg"
/>
}
title="Next.js"
href="/docs/nextjs"
href="/docs/sdks/nextjs"
>
{' '}
</Card>
@@ -110,7 +110,7 @@ Clears the current user identifier and ends the session.
/>
}
title="Remix"
href="/docs/remix"
href="/docs/sdks/remix"
>
{' '}
</Card>
@@ -119,7 +119,7 @@ Clears the current user identifier and ends the session.
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1024px-Vue.js_Logo_2.svg.png" />
}
title="Vue"
href="/docs/vue"
href="/docs/sdks/vue"
>
{' '}
</Card>
@@ -131,7 +131,7 @@ Clears the current user identifier and ends the session.
/>
}
title="Astro"
href="/docs/astro"
href="/docs/sdks/astro"
>
{' '}
</Card>
@@ -140,3 +140,28 @@ Clears the current user identifier and ends the session.
## Unofficial SDKs
While not officially supported, the following community-contributed SDKs are available.
<Cards>
<Card
icon={
<BrandLogo
src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Laravel.svg/1200px-Laravel.svg.png"
/>
}
title="Laravel"
href="https://github.com/tbleckert/openpanel-laravel"
>
{' '}
</Card>
<Card
icon={
<BrandLogo
src="https://storage.googleapis.com/cms-storage-bucket/0dbfcc7a59cd1cf16282.png"
/>
}
title="Flutter"
href="https://github.com/stevenosse/openpanel_flutter"
>
{' '}
</Card>
</Cards>

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# Node
Use <Link href="/docs/javascript">Javascript SDK</Link> to track events in Node.

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# React
Use <Link href="/docs/script">script tag</Link> or <Link href="/docs/web">Web SDK</Link> for now. We'll add a dedicated react sdk soon.

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# Remix
Use <Link href="/docs/script">script tag</Link> or <Link href="/docs/web">Web SDK</Link> for now. We'll add a dedicated remix sdk soon.

View File

@@ -0,0 +1,19 @@
{
"script": "Web (Script Tag)",
"web": "Web (Module)",
"nextjs": "Next.js",
"react": "React",
"react-native": "React-Native",
"remix": "Remix",
"vue": "Vue",
"astro": "Astro",
"node": "Node",
"express": "Express (backend)",
"-- Others": {
"type": "separator",
"title": "Others"
},
"javascript": "JavaScript",
"api": "API",
"export": "Export"
}

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# Astro
You can use <Link href="/docs/sdks/script">script tag</Link> or <Link href="/docs/sdks/web">Web SDK</Link> to track events in Astro.

View File

@@ -7,7 +7,7 @@ import CommonSdkConfig from 'src/components/common-sdk-config.mdx';
# Express
The Express middleware is a basic wrapper around [Javascript SDK](/docs/javascript). It provides a simple way to add the SDK to your Express application.
The Express middleware is a basic wrapper around [Javascript SDK](/docs/sdks/javascript). It provides a simple way to add the SDK to your Express application.
## Installation

View File

@@ -121,7 +121,7 @@ export const op = new Openpanel({
op.track('my_event', { foo: 'bar' });
```
Refer to the [Javascript SDK](/docs/javascript#usage) for usage instructions.
Refer to the [Javascript SDK](/docs/sdks/javascript#usage) for usage instructions.
### Tracking Events

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# Node
Use <Link href="/docs/sdks/javascript">Javascript SDK</Link> to track events in Node.

View File

@@ -108,4 +108,4 @@ op.track('my_event', { foo: 'bar' });
</Tabs.Tab>
</Tabs>
For more information on how to use the SDK, check out the [Javascript SDK](/docs/javascript#usage).
For more information on how to use the SDK, check out the [Javascript SDK](/docs/sdks/javascript#usage).

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# React
Use <Link href="/docs/sdks/script">script tag</Link> or <Link href="/docs/sdks/web">Web SDK</Link> for now. We'll add a dedicated react sdk soon.

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# Remix
Use <Link href="/docs/sdks/script">script tag</Link> or <Link href="/docs/sdks/web">Web SDK</Link> for now. We'll add a dedicated remix sdk soon.

View File

@@ -0,0 +1,5 @@
import Link from 'next/link';
# Vue
Use <Link href="/docs/sdks/script">script tag</Link> or <Link href="/docs/sdks/web">Web SDK</Link> for now. We'll add a dedicated react sdk soon.

View File

@@ -45,4 +45,4 @@ op.track('my_event', { foo: 'bar' });
## Usage
Refer to the [Javascript SDK](/docs/javascript#usage) for usage instructions.
Refer to the [Javascript SDK](/docs/sdks/javascript#usage) for usage instructions.

View File

@@ -0,0 +1,3 @@
{
"index": "Get started"
}

View File

@@ -0,0 +1,121 @@
import { Callout, Tabs, Steps } from 'nextra/components';
# Self-hosting
<Callout>OpenPanel is not stable yet. If you still want to self-host you can go ahead. Bear in mind that new changes might give a little headache to keep up with.</Callout>
This is a simple guide how to get started with OpenPanel on your own VPS.
## Instructions
### Prerequisites
- VPS of any kind (only tested on Ubuntu 24.04)
- [Clerk.com](https://clerk.com) account (they have a free tier)
### Quickstart
```bash
git clone https://github.com/Openpanel-dev/openpanel && cd openpanel/self-hosting && ./start
# After setup is complete run `./start` to start OpenPanel
```
<Steps>
### Clone
Clone the repository to your VPS
```bash
git clone https://github.com/Openpanel-dev/openpanel.git
```
### Run the setup script
The setup script will do 3 things
1. Install node (if you accept)
2. Install docker (if you accept)
3. Execute a node script that will ask some questions about your setup
4. After this is done you'll need to point a webhook inside Clerk (https://your-domain.com/api/webhook/clerk)
> Setup takes 5-10 minutes depending on your VPS. It'll build all the docker images.
```bash
cd openpanel/self-hosting
./setup
```
### Start 🚀
Run the `./start` script located inside the self-hosting folder
```bash
./start
```
</Steps>
## Clerk.com
<Callout>
Some might wonder why we use Clerk.com for authentication. The main reason for this is that Clerk have great support for iOS and Android apps. We're in the process of building an iOS app and we want to have a seamless experience for our users.
**next-auth** is great, but lacks good support for mobile apps.
</Callout>
You'll need to create an account at [Clerk.com](https://clerk.com) and create a new project. You'll need the 3 keys that Clerk provides you with.
- **Publishable key** `pk_live_xxx`
- **Secret key** `sk_live_xxx`
- **Signing secret** `"whsec_xxx"`
### Webhooks
You'll also need to add a webhook to your domain. We listen on some events from Clerk to keep our database in sync.
#### URL
- **Path**: `/api/webhook/clerk`
- **Example**: `https://your-domain.com/api/webhook/clerk`
#### Events we listen to
- `organizationMembership.created`
- `user.created`
- `organizationMembership.deleted`
- `user.updated`
- `user.deleted`
## Good to know
### Always use correct api url
When self-hosting you'll need to provide your api url when initializing the SDK.
The path should be `/api` and the domain should be your domain.
```html filename="index.html" {4}
<script>
window.op = window.op||function(...args){(window.op.q=window.op.q||[]).push(args);};
window.op('init', {
apiUrl: 'https://your-domain.com/api',
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
</script>
<script src="https://openpanel.dev/op1.js" defer async></script>
```
```js filename="op.ts" {4}
import { OpenPanel } from '@openpanel/sdk';
const op = new OpenPanel({
apiUrl: 'https://your-domain.com/api',
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```

View File

@@ -1,5 +0,0 @@
import Link from 'next/link';
# Vue
Use <Link href="/docs/script">script tag</Link> or <Link href="/docs/web">Web SDK</Link> for now. We'll add a dedicated vue sdk soon.

View File

@@ -1,3 +1,5 @@
<img src="https://openpanel.dev/ogimage.png" />
# Introduction
Openpanel is an open-source alternative to Mixpanel. Combining the power of Mixpanel with the ease of Plausible, Openpanel is a privacy-focused analytics tool that gives you the insights you need to make data-driven decisions.

View File

@@ -19,7 +19,7 @@ export default {
height="32"
width="32"
/>
<strong style={{ marginLeft: '8px' }}>openpanel</strong>
<strong style={{ marginLeft: '8px' }}>OpenPanel</strong>
</>
),
head: () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 428 KiB

View File

@@ -1,85 +1,77 @@
ARG NODE_VERSION=20.15.1
FROM --platform=linux/amd64 node:${NODE_VERSION}-slim AS base
FROM node:${NODE_VERSION}-slim AS base
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN apt update \
&& apt install -y curl \
&& curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \
&& bash n $NODE_VERSION \
&& rm n \
&& npm install -g n
RUN corepack enable && \
apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
openssl \
libssl3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json package.json
COPY pnpm-lock.yaml pnpm-lock.yaml
COPY pnpm-workspace.yaml pnpm-workspace.yaml
COPY apps/worker/package.json apps/worker/package.json
COPY packages/db/package.json packages/db/package.json
COPY packages/redis/package.json packages/redis/package.json
COPY packages/logger/package.json packages/logger/package.json
COPY packages/queue/package.json packages/queue/package.json
COPY packages/common/package.json packages/common/package.json
COPY packages/constants/package.json packages/constants/package.json
COPY packages/validation/package.json packages/validation/package.json
COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json
COPY patches patches
# Workspace
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# Apps
COPY apps/worker/package.json ./apps/worker/
# Packages
COPY packages/db/package.json ./packages/db/
COPY packages/redis/package.json ./packages/redis/
COPY packages/queue/package.json ./packages/queue/
COPY packages/logger/package.json ./packages/logger/
COPY packages/common/package.json ./packages/common/
COPY packages/constants/package.json ./packages/constants/
# Patches
COPY patches ./patches
# BUILD
FROM base AS build
WORKDIR /app/apps/worker
RUN pnpm install --frozen-lockfile --ignore-scripts
WORKDIR /app
RUN pnpm install --frozen-lockfile && \
pnpm store prune
WORKDIR /app
COPY apps/worker apps/worker
COPY packages packages
COPY tooling tooling
RUN pnpm db:codegen
COPY apps/worker ./apps/worker
COPY packages ./packages
COPY tooling ./tooling
WORKDIR /app/apps/worker
RUN pnpm run build
RUN pnpm db:codegen && \
pnpm --filter worker run build
# PROD
FROM base AS prod
WORKDIR /app/apps/worker
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
WORKDIR /app
COPY --from=build /app/package.json ./
COPY --from=build /app/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod && \
pnpm store prune
# FINAL
FROM base AS runner
COPY --from=build /app/package.json /app/package.json
COPY --from=prod /app/node_modules /app/node_modules
WORKDIR /app
COPY --from=build /app/package.json ./
COPY --from=prod /app/node_modules ./node_modules
# Apps
COPY --from=build /app/apps/worker /app/apps/worker
# Apps node_modules
COPY --from=prod /app/apps/worker/node_modules /app/apps/worker/node_modules
COPY --from=build /app/apps/worker ./apps/worker
# Packages
COPY --from=build /app/packages/db /app/packages/db
COPY --from=build /app/packages/redis /app/packages/redis
COPY --from=build /app/packages/logger /app/packages/logger
COPY --from=build /app/packages/queue /app/packages/queue
COPY --from=build /app/packages/common /app/packages/common
# Packages node_modules
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules
COPY --from=prod /app/packages/logger/node_modules /app/packages/logger/node_modules
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules
COPY --from=build /app/packages/db ./packages/db
COPY --from=build /app/packages/redis ./packages/redis
COPY --from=build /app/packages/logger ./packages/logger
COPY --from=build /app/packages/queue ./packages/queue
COPY --from=build /app/packages/common ./packages/common
RUN pnpm db:codegen

View File

@@ -12,8 +12,8 @@
},
"dependencies": {
"@baselime/pino-transport": "^0.1.5",
"@bull-board/api": "^5.21.0",
"@bull-board/express": "^5.21.0",
"@bull-board/api": "5.21.0",
"@bull-board/express": "5.21.0",
"@openpanel/common": "workspace:*",
"@openpanel/db": "workspace:*",
"@openpanel/logger": "workspace:*",

View File

@@ -5,6 +5,7 @@ import type { WorkerOptions } from 'bullmq';
import { Worker } from 'bullmq';
import express from 'express';
import { createInitialSalts } from '@openpanel/db';
import { cronQueue, eventsQueue, sessionsQueue } from '@openpanel/queue';
import { getRedisQueue } from '@openpanel/redis';
@@ -15,7 +16,7 @@ import { register } from './metrics';
const PORT = parseInt(process.env.WORKER_PORT || '3000', 10);
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath(process.env.SELF_HOSTED ? '/worker' : '/');
serverAdapter.setBasePath('/');
const app = express();
const workerOptions: WorkerOptions = {
@@ -162,10 +163,28 @@ async function start() {
}
);
if (process.env.SELF_HOSTED && process.env.NODE_ENV === 'production') {
await cronQueue.add(
'ping',
{
type: 'ping',
payload: undefined,
},
{
jobId: 'ping',
repeat: {
pattern: '0 0 * * *',
},
}
);
}
const repeatableJobs = await cronQueue.getRepeatableJobs();
console.log('Repeatable jobs:');
console.log(repeatableJobs);
await createInitialSalts();
}
start();

View File

@@ -0,0 +1,26 @@
import { chQuery, TABLE_NAMES } from '@openpanel/db';
export async function ping() {
const [res] = await chQuery<{ count: number }>(
`SELECT COUNT(*) as count FROM ${TABLE_NAMES.events}`
);
if (typeof res?.count === 'number') {
const response = await fetch('https://api.openpanel.com/misc/ping', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
domain: process.env.NEXT_PUBLIC_DASHBOARD_URL,
count: res?.count,
}),
});
if (response.ok) {
return await response.json();
}
throw new Error('Failed to ping the server');
}
}

View File

@@ -3,6 +3,7 @@ import type { Job } from 'bullmq';
import { eventBuffer, profileBuffer } from '@openpanel/db';
import type { CronQueuePayload } from '@openpanel/queue';
import { ping } from './cron.ping';
import { salt } from './cron.salt';
export async function cronJob(job: Job<CronQueuePayload>) {
@@ -16,5 +17,8 @@ export async function cronJob(job: Job<CronQueuePayload>) {
case 'flushProfiles': {
return await profileBuffer.flush();
}
case 'ping': {
return await ping();
}
}
}