feature(public,docs): new public website and docs
This commit is contained in:
39
apps/docs/.gitignore
vendored
39
apps/docs/.gitignore
vendored
@@ -1,39 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
@@ -1 +0,0 @@
|
||||
./src/pages/docs/*
|
||||
@@ -1,59 +0,0 @@
|
||||
FROM --platform=linux/amd64 node:20-slim AS base
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
ARG NODE_VERSION=20.15.1
|
||||
|
||||
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
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package.json
|
||||
COPY pnpm-lock.yaml pnpm-lock.yaml
|
||||
COPY pnpm-workspace.yaml pnpm-workspace.yaml
|
||||
COPY apps/docs/package.json apps/docs/package.json
|
||||
|
||||
# BUILD
|
||||
FROM base AS build
|
||||
|
||||
WORKDIR /app/apps/docs
|
||||
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
WORKDIR /app
|
||||
COPY apps apps
|
||||
RUN mkdir packages
|
||||
COPY tooling tooling
|
||||
|
||||
WORKDIR /app/apps/docs
|
||||
RUN pnpm run build
|
||||
|
||||
# PROD
|
||||
FROM base AS prod
|
||||
|
||||
WORKDIR /app/apps/docs
|
||||
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
|
||||
|
||||
# FINAL
|
||||
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/docs /app/apps/docs
|
||||
# Apps node_modules
|
||||
COPY --from=prod /app/apps/docs/node_modules /app/apps/docs/node_modules
|
||||
|
||||
WORKDIR /app/apps/docs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["pnpm", "start"]
|
||||
@@ -1 +0,0 @@
|
||||
# Docs
|
||||
@@ -1,33 +0,0 @@
|
||||
import nextra from 'nextra';
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ['@openpanel/queue'],
|
||||
eslint: { ignoreDuringBuilds: true },
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
experimental: {
|
||||
// Avoid "Critical dependency: the request of a dependency is an expression"
|
||||
serverComponentsExternalPackages: ['bullmq'],
|
||||
},
|
||||
/**
|
||||
* If you are using `appDir` then you must comment the below `i18n` config out.
|
||||
*
|
||||
* @see https://github.com/vercel/next.js/issues/41980
|
||||
*/
|
||||
i18n: {
|
||||
locales: ['en-US'],
|
||||
defaultLocale: 'en-US',
|
||||
},
|
||||
};
|
||||
|
||||
const withNextra = nextra({
|
||||
theme: 'nextra-theme-docs',
|
||||
themeConfig: './theme.config.jsx',
|
||||
flexsearch: {
|
||||
codeblocks: false,
|
||||
},
|
||||
defaultShowCopyCode: true,
|
||||
});
|
||||
|
||||
export default withNextra(config);
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "@openpanel/docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "rm -rf .next && pnpm with-env next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env -c --"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "~14.2.1",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 424 KiB |
@@ -1,15 +0,0 @@
|
||||
interface Props {
|
||||
src: string;
|
||||
isDark?: boolean;
|
||||
}
|
||||
export function BrandLogo({ src, isDark }: Props) {
|
||||
if (isDark) {
|
||||
return (
|
||||
<div className="h-9 w-9 rounded-full bg-white p-1">
|
||||
<img className="h-full w-full object-contain" src={src} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <img className="h-9 w-9 object-contain" src={src} />;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
##### Common options
|
||||
|
||||
- `apiUrl` - The url of the openpanel API or your self-hosted instance
|
||||
- `clientId` - The client id of your application
|
||||
- `clientSecret` - The client secret of your application (**only required for server-side events**)
|
||||
- `filter` - A function that will be called before sending an event. If it returns false, the event will not be sent
|
||||
- `disabled` - If true, the library will not send any events
|
||||
- `waitForProfile` - If true, the library will wait for the profile to be set before sending events
|
||||
@@ -1,12 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
export function DeviceIdWarning() {
|
||||
return (
|
||||
<Callout>
|
||||
Read more about{' '}
|
||||
<Link href="/docs/device-id">device id and why you might want it</Link>.
|
||||
**We recommend not to but it's up to you.**
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
export function PersonalDataWarning() {
|
||||
return (
|
||||
<Callout emoji="⚠️">
|
||||
Keep in mind that this is considered personal data. Make sure you have the
|
||||
users consent before calling this!
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
##### Web options
|
||||
|
||||
- `trackScreenViews` - If true, the library will automatically track screen views (default: false)
|
||||
- `trackOutgoingLinks` - If true, the library will automatically track outgoing links (default: false)
|
||||
- `trackAttributes` - If true, you can trigger events by using html attributes (`<button type="button" data-track="your_event" />`) (default: false)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
article main a {
|
||||
@apply underline;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import Script from 'next/script';
|
||||
|
||||
import 'src/globals.css';
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Component {...pageProps} />
|
||||
<Script
|
||||
src="https://openpanel.dev/op1.js"
|
||||
async
|
||||
defer
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script
|
||||
id="openpanel"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.op =
|
||||
window.op ||
|
||||
function (...args) {
|
||||
(window.op.q = window.op.q || []).push(args);
|
||||
};
|
||||
window.op('ctor', {
|
||||
clientId: '301c6dc1-424c-4bc3-9886-a8beab09b615',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"index": {
|
||||
"title": "Home",
|
||||
"type": "page"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Documentation",
|
||||
"type": "page"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"index": "Get Started",
|
||||
"sdks": "SDKs",
|
||||
"migration": "Migrations",
|
||||
"self-hosting": "Self-hosting"
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { Callout, Card, Cards } from 'nextra/components';
|
||||
import { BrandLogo } from 'src/components/brand-logo';
|
||||
|
||||
# Documentation
|
||||
|
||||
The OpenPanel SDKs provide a set of core methods that allow you to track events, identify users, and more. Here's an overview of the key methods available in the SDKs.
|
||||
|
||||
<Callout>
|
||||
While all OpenPanel SDKs share a common set of core methods, some may have
|
||||
syntax variations or additional methods specific to their environment. This
|
||||
documentation provides an overview of the base methods and available SDKs.
|
||||
</Callout>
|
||||
|
||||
## Core Methods
|
||||
|
||||
### setGlobalProperties
|
||||
|
||||
Sets global properties that will be included with every subsequent event.
|
||||
|
||||
### track
|
||||
|
||||
Tracks a custom event with the given name and optional properties.
|
||||
|
||||
**Tips**
|
||||
|
||||
You can identify the user directly with this method.
|
||||
|
||||
```js filename="Example shown in JavaScript"
|
||||
track('your_event_name', {
|
||||
foo: 'bar',
|
||||
baz: 'qux',
|
||||
// reserved property name
|
||||
__identify: {
|
||||
profileId: 'your_user_id', // required
|
||||
email: 'your_user_email',
|
||||
firstName: 'your_user_name',
|
||||
lastName: 'your_user_name',
|
||||
avatar: 'your_user_avatar',
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### identify
|
||||
|
||||
Associates the current user with a unique identifier and optional traits.
|
||||
|
||||
### alias
|
||||
|
||||
Creates an alias for a user identifier.
|
||||
|
||||
### increment
|
||||
|
||||
Increments a numeric property for a user.
|
||||
|
||||
### decrement
|
||||
|
||||
Decrements a numeric property for a user.
|
||||
|
||||
### clear
|
||||
|
||||
Clears the current user identifier and ends the session.
|
||||
|
||||
## Official SDKs
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
icon={
|
||||
<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/sdks/script"
|
||||
>
|
||||
{' '}
|
||||
</Card>
|
||||
<Card
|
||||
icon={
|
||||
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png" />
|
||||
}
|
||||
title="React"
|
||||
href="/docs/sdks/react"
|
||||
>
|
||||
{' '}
|
||||
</Card>
|
||||
<Card
|
||||
icon={
|
||||
<BrandLogo src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png" />
|
||||
}
|
||||
title="React-Native"
|
||||
href="/docs/sdks/react-native"
|
||||
>
|
||||
{' '}
|
||||
</Card>
|
||||
<Card
|
||||
icon={
|
||||
<BrandLogo
|
||||
isDark
|
||||
src="https://pbs.twimg.com/profile_images/1565710214019444737/if82cpbS_400x400.jpg"
|
||||
/>
|
||||
}
|
||||
title="Next.js"
|
||||
href="/docs/sdks/nextjs"
|
||||
>
|
||||
{' '}
|
||||
</Card>
|
||||
<Card
|
||||
icon={
|
||||
<BrandLogo
|
||||
isDark
|
||||
src="https://www.datocms-assets.com/205/1642515307-square-logo.svg"
|
||||
/>
|
||||
}
|
||||
title="Remix"
|
||||
href="/docs/sdks/remix"
|
||||
>
|
||||
{' '}
|
||||
</Card>
|
||||
<Card
|
||||
icon={
|
||||
<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/sdks/vue"
|
||||
>
|
||||
{' '}
|
||||
</Card>
|
||||
<Card
|
||||
icon={
|
||||
<BrandLogo
|
||||
isDark
|
||||
src="https://astro.build/assets/press/astro-icon-dark.png"
|
||||
/>
|
||||
}
|
||||
title="Astro"
|
||||
href="/docs/sdks/astro"
|
||||
>
|
||||
{' '}
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## 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>
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"beta-v1": "Beta to v1"
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
# Migrate from `beta` to `v1`
|
||||
|
||||
We are happy to announce the release of `v1` of the Openpanel SDK. This release includes a lot of improvements and changes to the SDK. This guide will help you migrate from the `beta` version to the `v1` version.
|
||||
|
||||
## General
|
||||
|
||||
The `Openpanel` class is now called `OpenPanel`!
|
||||
|
||||
## Options
|
||||
|
||||
- Renamed: `api` to `apiUrl`
|
||||
- Added: `disabled`
|
||||
- Added: `filter`
|
||||
|
||||
## Methods
|
||||
- Renamed: `event` method is now called `track`
|
||||
- Renamed: `setProfile` and `setProfileId` is now called `identify` (and combined)
|
||||
- Changed: `increment('app_opened', 5)` is now `increment({ name: 'app_opened', value: 5, profileId: '123' })`. So profile ID is now required.
|
||||
- Changed: `decrement('app_opened', 5)` is now `decrement({ name: 'app_opened', value: 5, profileId: '123' })`. So profile ID is now required.
|
||||
- Improved: `screenView` method has 2 arguments now. This change is more aligned with `@openpanel/react-native`.
|
||||
```ts
|
||||
screenView(properties?: TrackProperties): void;
|
||||
screenView(path: string, properties?: TrackProperties): void;
|
||||
|
||||
// Example
|
||||
op.screenView('/home', { title: 'Home' }); // path will be "/home"
|
||||
op.screenView({ title: 'Home' }); // path will be what ever window.location.pathname is
|
||||
```
|
||||
|
||||
## Script tag
|
||||
|
||||
- New: `https://openpanel.dev/op1.js` should be used instead of `op.js` (note the filename)
|
||||
- Renamed: Tracking with attributes have changed. Use `data-track="my_event"` instead of `data-event="my_event"`
|
||||
|
||||
## @openpanel/nextjs
|
||||
|
||||
- Renamed: `OpenpanelProvider` to `OpenPanelComponent`
|
||||
- Removed: All exported methods (trackEvent etc). Use the `useOpenPanel` hook instead since these are client tracking only
|
||||
- Moved: `createNextRouteHandler` is moved to `@openpanel/nextjs/server`
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
# OpenPanel REST API with cURL
|
||||
|
||||
This guide demonstrates how to interact with the OpenPanel API using cURL. These examples provide a low-level understanding of the API endpoints and can be useful for testing or for integrations where a full SDK isn't available.
|
||||
|
||||
## Good to know
|
||||
|
||||
- If you want to track **geo location** you'll need to pass the `ip` property as a header `x-client-ip`
|
||||
- If you want to track **device information** you'll need to pass the `user-agent` property as a header `user-agent`
|
||||
|
||||
## Authentication
|
||||
|
||||
All requests to the OpenPanel API require authentication. You'll need to include your `clientId` and `clientSecret` in the headers of each request.
|
||||
|
||||
```bash
|
||||
-H "openpanel-client-id: YOUR_CLIENT_ID" \
|
||||
-H "openpanel-client-secret: YOUR_CLIENT_SECRET"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Base URL
|
||||
|
||||
All API requests should be made to:
|
||||
|
||||
```
|
||||
https://api.openpanel.dev
|
||||
```
|
||||
|
||||
### Tracking Events
|
||||
|
||||
To track an event:
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.openpanel.dev/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "openpanel-client-id: YOUR_CLIENT_ID" \
|
||||
-H "openpanel-client-secret: YOUR_CLIENT_SECRET" \
|
||||
-d '{
|
||||
"type": "track",
|
||||
"payload": {
|
||||
"name": "my_event",
|
||||
"properties": {
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Identifying Users
|
||||
|
||||
To identify a user:
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.openpanel.dev/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "openpanel-client-id: YOUR_CLIENT_ID" \
|
||||
-H "openpanel-client-secret: YOUR_CLIENT_SECRET" \
|
||||
-d '{
|
||||
"type": "identify",
|
||||
"payload": {
|
||||
"profileId": "123",
|
||||
"firstName": "Joe",
|
||||
"lastName": "Doe",
|
||||
"email": "joe@doe.com",
|
||||
"properties": {
|
||||
"tier": "premium"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Creating Aliases
|
||||
To create an alias for a user:
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.openpanel.dev/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "openpanel-client-id: YOUR_CLIENT_ID" \
|
||||
-H "openpanel-client-secret: YOUR_CLIENT_SECRET" \
|
||||
-d '{
|
||||
"type": "alias",
|
||||
"payload": {
|
||||
"profileId": "1",
|
||||
"alias": "a1"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Incrementing Properties
|
||||
To increment a numeric property:
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.openpanel.dev/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "openpanel-client-id: YOUR_CLIENT_ID" \
|
||||
-H "openpanel-client-secret: YOUR_CLIENT_SECRET" \
|
||||
-d '{
|
||||
"type": "increment",
|
||||
"payload": {
|
||||
"profileId": "1",
|
||||
"property": "visits",
|
||||
"value": 1
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Decrementing Properties
|
||||
To decrement a numeric property:
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.openpanel.dev/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "openpanel-client-id: YOUR_CLIENT_ID" \
|
||||
-H "openpanel-client-secret: YOUR_CLIENT_SECRET" \
|
||||
-d '{
|
||||
"type": "decrement",
|
||||
"payload": {
|
||||
"profileId": "1",
|
||||
"property": "visits",
|
||||
"value": 1
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
The API uses standard HTTP response codes to indicate the success or failure of requests. In case of an error, the response body will contain more information about the error.
|
||||
Example error response:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Invalid client credentials",
|
||||
"status": 401
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
The API implements rate limiting to prevent abuse. If you exceed the rate limit, you'll receive a 429 (Too Many Requests) response. The response will include headers indicating your rate limit status.
|
||||
|
||||
Best Practices
|
||||
1. Always use HTTPS to ensure secure communication.
|
||||
2. Store your clientId and clientSecret securely and never expose them in client-side code.
|
||||
3. Implement proper error handling in your applications.
|
||||
4. Respect rate limits and implement exponential backoff for retries.
|
||||
@@ -1,5 +0,0 @@
|
||||
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.
|
||||
@@ -1,176 +0,0 @@
|
||||
# Export API
|
||||
|
||||
The Export API allows you to retrieve event data and chart data from your OpenPanel projects.
|
||||
|
||||
## Authentication
|
||||
|
||||
To authenticate with the Export API, you need to use your `clientId` and `clientSecret`. Make sure your client has `read` or `root` mode. The default client does not have access to the Export API.
|
||||
|
||||
Include the following headers with your requests:
|
||||
- `openpanel-client-id`: Your OpenPanel client ID
|
||||
- `openpanel-client-secret`: Your OpenPanel client secret
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
curl 'https://api.openpanel.dev/export/events' \
|
||||
-H 'openpanel-client-id: YOUR_CLIENT_ID' \
|
||||
-H 'openpanel-client-secret: YOUR_CLIENT_SECRET'
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
Get events from a specific project within a date range.
|
||||
|
||||
Endpoint: `GET /export/events`
|
||||
|
||||
Parameters:
|
||||
- project_id (required): The ID of the project
|
||||
- event (optional): Filter by event name(s). Can be a single event or an array of events.
|
||||
- start (optional): Start date (format: YYYY-MM-DD)
|
||||
- end (optional): End date (format: YYYY-MM-DD)
|
||||
- page (optional, default: 1): Page number for pagination
|
||||
- limit (optional, default: 50, max: 50): Number of events per page
|
||||
- includes (optional): Additional fields to include in the response
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
curl 'https://api.openpanel.dev/export/events?project_id=abc&event=screen_view&start=2024-04-15&end=2024-04-18' \
|
||||
-H 'openpanel-client-id: YOUR_CLIENT_ID' \
|
||||
-H 'openpanel-client-secret: YOUR_CLIENT_SECRET'
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Example |
|
||||
|-----------|------|-------------|---------|
|
||||
| projectId | string | The ID of the project to fetch events from | `abc123` |
|
||||
| event | string or string[] | Event name(s) to filter | `screen_view` or `["screen_view","button_click"]` |
|
||||
| start | string | Start date for the event range (ISO format) | `2024-04-15` |
|
||||
| end | string | End date for the event range (ISO format) | `2024-04-18` |
|
||||
| page | number | Page number for pagination (default: 1) | `2` |
|
||||
| limit | number | Number of events per page (default: 50, max: 50) | `25` |
|
||||
| includes | string or string[] | Additional fields to include in the response | `profile` or `["profile","meta"]` |
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl 'https://api.openpanel.dev/export/events?project_id=abc123&event=screen_view&start=2024-04-15&end=2024-04-18&page=1&limit=50&includes=profile,meta' \
|
||||
-H 'openpanel-client-id: YOUR_CLIENT_ID' \
|
||||
-H 'openpanel-client-secret: YOUR_CLIENT_SECRET'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"count": number,
|
||||
"totalCount": number,
|
||||
"pages": number,
|
||||
"current": number
|
||||
},
|
||||
"data": Array<Event>
|
||||
}
|
||||
```
|
||||
|
||||
## Charts
|
||||
|
||||
Retrieve chart data for a specific project.
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
GET /export/charts
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Example |
|
||||
|-----------|------|-------------|---------|
|
||||
| projectId | string | The ID of the project to fetch chart data from | `abc123` |
|
||||
| events | string[] | Array of event names to include in the chart | `["sign_up","purchase"]` |
|
||||
| breakdowns | object[] | Array of breakdown configurations | `[{"name":"country"}]` |
|
||||
| interval | string | Time interval for data points | `day` |
|
||||
| range | string | Predefined date range | `last_7_days` |
|
||||
| previous | boolean | Include data from the previous period | `true` |
|
||||
| startDate | string | Custom start date (ISO format) | `2024-04-01` |
|
||||
| endDate | string | Custom end date (ISO format) | `2024-04-30` |
|
||||
| chartType | string | Type of chart to generate | `linear` |
|
||||
| metric | string | Metric to use for calculations | `sum` |
|
||||
| limit | number | Limit the number of results | `10` |
|
||||
| offset | number | Offset for pagination | `0` |
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl 'https://api.openpanel.dev/export/charts?projectId=abc123&events=["sign_up","purchase"]&interval=day&range=last_30_days&chartType=linear&metric=sum' \
|
||||
-H 'openpanel-client-id: YOUR_CLIENT_ID' \
|
||||
-H 'openpanel-client-secret: YOUR_CLIENT_SECRET'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
The response will include chart data with series, metrics, and optional previous period comparisons based on the input parameters.
|
||||
|
||||
## Funnel
|
||||
|
||||
Retrieve funnel data for a specific project.
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
GET /export/funnel
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Example |
|
||||
|-----------|------|-------------|---------|
|
||||
| projectId | string | The ID of the project to fetch funnel data from | `abc123` |
|
||||
| events | object[] | Array of event configurations for the funnel steps | `[{"name":"sign_up","filters":[]}]` |
|
||||
| range | string | Predefined date range | `last_30_days` |
|
||||
| startDate | string | Custom start date (ISO format) | `2024-04-01` |
|
||||
| endDate | string | Custom end date (ISO format) | `2024-04-30` |
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl 'https://api.openpanel.dev/export/funnel?projectId=abc123&events=[{"name":"sign_up"},{"name":"purchase"}]&range=last_30_days' \
|
||||
-H 'openpanel-client-id: YOUR_CLIENT_ID' \
|
||||
-H 'openpanel-client-secret: YOUR_CLIENT_SECRET'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
The response will include funnel data with total sessions and step-by-step breakdown of the funnel progression.
|
||||
|
||||
```json
|
||||
{
|
||||
"totalSessions": number,
|
||||
"steps": [
|
||||
{
|
||||
"event": {
|
||||
"name": string,
|
||||
"displayName": string
|
||||
},
|
||||
"count": number,
|
||||
"percent": number,
|
||||
"dropoffCount": number,
|
||||
"dropoffPercent": number,
|
||||
"previousCount": number
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All date parameters should be in ISO format (YYYY-MM-DD).
|
||||
- The `range` parameter accepts values like `today`, `yesterday`, `last_7_days`, `last_30_days`, `this_month`, `last_month`, `this_year`, `last_year`, `all_time`.
|
||||
- The `interval` parameter accepts values like `minute`, `hour`, `day`, `month`.
|
||||
- The `chartType` parameter can be `linear` or other supported chart types.
|
||||
- The `metric` parameter can be `sum`, `average`, `min`, or `max`.
|
||||
|
||||
Remember to replace `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` with your actual OpenPanel API credentials.
|
||||
@@ -1,80 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
import { Callout, Steps, Tabs } from 'nextra/components';
|
||||
import { DeviceIdWarning } from 'src/components/device-id-warning';
|
||||
import { PersonalDataWarning } from 'src/components/personal-data-warning';
|
||||
|
||||
import CommonSdkConfig from 'src/components/common-sdk-config.mdx';
|
||||
|
||||
# Express
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
pnpm install @openpanel/express
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The default export of `@openpanel/express` is a function that returns an Express middleware. It will also append the Openpanel SDK to the `req` object.
|
||||
|
||||
You can access it via `req.op`.
|
||||
|
||||
```ts
|
||||
import express from 'express';
|
||||
|
||||
import createOpenpanelMiddleware from '@openpanel/express';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
createOpenpanelMiddleware({
|
||||
clientId: 'xxx',
|
||||
clientSecret: 'xxx',
|
||||
// trackRequest(url) {
|
||||
// return url.includes('/v1')
|
||||
// },
|
||||
// getProfileId(req) {
|
||||
// return req.user.id
|
||||
// }
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/sign-up', (req, res) => {
|
||||
// track sign up events
|
||||
req.op.track('sign-up', {
|
||||
email: req.body.email,
|
||||
});
|
||||
res.send('Hello World');
|
||||
});
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log('Server is running on http://localhost:3000');
|
||||
});
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
<CommonSdkConfig />
|
||||
|
||||
#### Express options
|
||||
|
||||
- `trackRequest` - A function that returns `true` if the request should be tracked.
|
||||
- `getProfileId` - A function that returns the profile ID of the user making the request.
|
||||
|
||||
## Typescript
|
||||
|
||||
If `req.op` is not typed you can extend the `Request` interface.
|
||||
|
||||
```ts
|
||||
import { OpenPanel } from '@openpanel/express';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
export interface Request {
|
||||
op: OpenPanel;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,141 +0,0 @@
|
||||
import { Callout, Tabs, Steps } from 'nextra/components';
|
||||
import { PersonalDataWarning } from 'src/components/personal-data-warning';
|
||||
import CommonSdkConfig from 'src/components/common-sdk-config.mdx';
|
||||
import WebSdkConfig from 'src/components/web-sdk-config.mdx';
|
||||
|
||||
# Javascript SDK
|
||||
|
||||
The OpenPanel Web SDK allows you to track user behavior on your website using a simple script tag. This guide provides instructions for installing and using the Web SDK in your project.
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
### Step 1: Install
|
||||
|
||||
```bash
|
||||
npm install @openpanel/sdk
|
||||
```
|
||||
|
||||
### Step 2: Initialize
|
||||
|
||||
```js filename="op.ts"
|
||||
import { OpenPanel } from '@openpanel/sdk';
|
||||
|
||||
const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
<CommonSdkConfig />
|
||||
|
||||
### Step 3: Usage
|
||||
|
||||
```js filename="main.ts"
|
||||
import { op } from './op.js';
|
||||
|
||||
op.track('my_event', { foo: 'bar' });
|
||||
```
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
### Tracking Events
|
||||
|
||||
You can track events with two different methods: by calling the `op.track( directly or by adding `data-track` attributes to your HTML elements.
|
||||
|
||||
```ts filename="index.ts"
|
||||
import { op } from './op.ts';
|
||||
|
||||
op.track('my_event', { foo: 'bar' });
|
||||
```
|
||||
|
||||
### Identifying Users
|
||||
|
||||
To identify a user, call the `op.identify( method with a unique identifier.
|
||||
|
||||
```js filename="index.js"
|
||||
import { op } from './op.ts';
|
||||
|
||||
op.identify({
|
||||
profileId: '123', // Required
|
||||
firstName: 'Joe',
|
||||
lastName: 'Doe',
|
||||
email: 'joe@doe.com',
|
||||
properties: {
|
||||
tier: 'premium',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Setting Global Properties
|
||||
|
||||
To set properties that will be sent with every event:
|
||||
|
||||
```js filename="index.js"
|
||||
import { op } from './op.ts'
|
||||
|
||||
op.setGlobalProperties({
|
||||
app_version: '1.0.2',
|
||||
environment: 'production',
|
||||
});
|
||||
```
|
||||
|
||||
### Creating Aliases
|
||||
|
||||
To create an alias for a user:
|
||||
|
||||
```js filename="index.js"
|
||||
import { op } from './op.ts'
|
||||
|
||||
op.alias({
|
||||
alias: 'a1',
|
||||
profileId: '1'
|
||||
});
|
||||
```
|
||||
|
||||
### Incrementing Properties
|
||||
|
||||
To increment a numeric property on a user profile.
|
||||
|
||||
- `value` is the amount to increment the property by. If not provided, the property will be incremented by 1.
|
||||
|
||||
```js filename="index.js"
|
||||
import { op } from './op.ts'
|
||||
|
||||
op.increment({
|
||||
profileId: '1',
|
||||
property: 'visits',
|
||||
value: 1 // optional
|
||||
});
|
||||
```
|
||||
|
||||
### Decrementing Properties
|
||||
|
||||
To decrement a numeric property on a user profile.
|
||||
|
||||
- `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1.
|
||||
|
||||
```js filename="index.js"
|
||||
import { op } from './op.ts'
|
||||
|
||||
op.decrement({
|
||||
profileId: '1',
|
||||
property: 'visits',
|
||||
value: 1 // optional
|
||||
});
|
||||
```
|
||||
|
||||
### Clearing User Data
|
||||
|
||||
To clear the current user's data:
|
||||
|
||||
```js filename="index.js"
|
||||
import { op } from './op.ts'
|
||||
|
||||
op.clear()
|
||||
```
|
||||
@@ -1,299 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
import { Callout, Steps, Tabs } from 'nextra/components';
|
||||
import { DeviceIdWarning } from 'src/components/device-id-warning';
|
||||
import { PersonalDataWarning } from 'src/components/personal-data-warning';
|
||||
|
||||
import CommonSdkConfig from 'src/components/common-sdk-config.mdx';
|
||||
import WebSdkConfig from 'src/components/web-sdk-config.mdx';
|
||||
|
||||
# Next.js
|
||||
|
||||
Keep in mind that all tracking here happens on the client!
|
||||
|
||||
Read more about server side tracking in the [Server Side Tracking](#track-server-events) section.
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
pnpm install @openpanel/nextjs
|
||||
```
|
||||
|
||||
### Initialize
|
||||
|
||||
Add `OpenPanelComponent` to your root layout component.
|
||||
|
||||
```tsx
|
||||
import { OpenPanelComponent } from '@openpanel/nextjs';
|
||||
|
||||
export default RootLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<OpenPanelComponent
|
||||
clientId="your-client-id"
|
||||
trackScreenViews={true}
|
||||
// trackAttributes={true}
|
||||
// trackOutgoingLinks={true}
|
||||
// If you have a user id, you can pass it here to identify the user
|
||||
// profileId={'123'}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
<CommonSdkConfig />
|
||||
<WebSdkConfig />
|
||||
|
||||
##### NextJS options
|
||||
|
||||
- `profileId` - If you have a user id, you can pass it here to identify the user
|
||||
- `cdnUrl` - The url to the OpenPanel SDK (default: `https://openpanel.dev/op1.js`)
|
||||
- `filter` - This is a function that will be called before tracking an event. If it returns false the event will not be tracked. [Read more](#filter)
|
||||
- `globalProperties` - This is an object of properties that will be sent with every event.
|
||||
|
||||
##### `filter`
|
||||
|
||||
This options needs to be a stringified function and cannot access any variables outside of the function.
|
||||
|
||||
```tsx
|
||||
<OpenPanelComponent
|
||||
clientId="your-client-id"
|
||||
filter={`
|
||||
function filter(event) {
|
||||
return event.name !== 'my_event';
|
||||
}
|
||||
`}
|
||||
/>
|
||||
```
|
||||
|
||||
To take advantage of typescript you can do the following. _Note `toString`_
|
||||
```tsx /.toString();/
|
||||
import { type OpenPanelOptions } from '@openpanel/nextjs';
|
||||
|
||||
const opFilter = ((event: TrackHandlerPayload) => {
|
||||
return event.type === 'track' && event.payload.name === 'my_event';
|
||||
}).toString();
|
||||
|
||||
<OpenPanelComponent
|
||||
clientId="your-client-id"
|
||||
filter={opFilter}
|
||||
/>
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
### Client components
|
||||
|
||||
For client components you can just use the `useOpenPanel` hook.
|
||||
|
||||
```tsx
|
||||
import { useOpenPanel } from '@openpanel/nextjs';
|
||||
|
||||
function YourComponent() {
|
||||
const op = useOpenPanel();
|
||||
|
||||
return <button type="button" onClick={() => op.track('my_event', { foo: 'bar' })}>Trigger event</button>
|
||||
}
|
||||
```
|
||||
|
||||
### Server components
|
||||
|
||||
Since you can't use hooks in server components, you need to create an instance of the SDK. This is exported from `@openpanel/nextjs`.
|
||||
|
||||
<Callout>Remember, your client secret is exposed here so do not use this on client side.</Callout>
|
||||
|
||||
```tsx filename="utils/op.ts"
|
||||
import { OpenPanel } from '@openpanel/nextjs';
|
||||
|
||||
export const op = new OpenPanel({
|
||||
clientId: 'your-client-id',
|
||||
clientSecret: 'your-client-secret',
|
||||
});
|
||||
|
||||
// Now you can use `op` to track events
|
||||
op.track('my_event', { foo: 'bar' });
|
||||
```
|
||||
|
||||
Refer to the [Javascript SDK](/docs/sdks/javascript#usage) for usage instructions.
|
||||
|
||||
### Tracking Events
|
||||
|
||||
You can track events with two different methods: by calling the `op.track( directly or by adding `data-track` attributes to your HTML elements.
|
||||
|
||||
```ts filename="index.ts"
|
||||
useOpenPanel().track('my_event', { foo: 'bar' });
|
||||
```
|
||||
|
||||
### Identifying Users
|
||||
|
||||
To identify a user, call the `op.identify( method with a unique identifier.
|
||||
|
||||
```js filename="index.js"
|
||||
useOpenPanel().identify({
|
||||
profileId: '123', // Required
|
||||
firstName: 'Joe',
|
||||
lastName: 'Doe',
|
||||
email: 'joe@doe.com',
|
||||
properties: {
|
||||
tier: 'premium',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### For server components
|
||||
|
||||
For server components you can use the `IdentifyComponent` component which is exported from `@openpanel/nextjs`.
|
||||
|
||||
> This component is great if you have the user data available on the server side.
|
||||
|
||||
```tsx filename="app/nested/layout.tsx"
|
||||
import { IdentifyComponent } from '@openpanel/nextjs';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
const user = await getCurrentUser()
|
||||
|
||||
return (
|
||||
<>
|
||||
<IdentifyComponent
|
||||
profileId={user.id}
|
||||
firstName={user.firstName}
|
||||
lastName={user.lastName}
|
||||
email={user.email}
|
||||
properties={{
|
||||
tier: 'premium',
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Setting Global Properties
|
||||
|
||||
To set properties that will be sent with every event:
|
||||
|
||||
```js filename="index.js"
|
||||
useOpenPanel().setGlobalProperties({
|
||||
app_version: '1.0.2',
|
||||
environment: 'production',
|
||||
});
|
||||
```
|
||||
|
||||
### Creating Aliases
|
||||
|
||||
To create an alias for a user:
|
||||
|
||||
```js filename="index.js"
|
||||
useOpenPanel().alias({
|
||||
alias: 'a1',
|
||||
profileId: '1'
|
||||
});
|
||||
```
|
||||
|
||||
### Incrementing Properties
|
||||
|
||||
To increment a numeric property on a user profile.
|
||||
|
||||
- `value` is the amount to increment the property by. If not provided, the property will be incremented by 1.
|
||||
|
||||
```js filename="index.js"
|
||||
useOpenPanel().increment({
|
||||
profileId: '1',
|
||||
property: 'visits',
|
||||
value: 1 // optional
|
||||
});
|
||||
```
|
||||
|
||||
### Decrementing Properties
|
||||
|
||||
To decrement a numeric property on a user profile.
|
||||
|
||||
- `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1.
|
||||
|
||||
```js filename="index.js"
|
||||
useOpenPanel().decrement({
|
||||
profileId: '1',
|
||||
property: 'visits',
|
||||
value: 1 // optional
|
||||
});
|
||||
```
|
||||
|
||||
### Clearing User Data
|
||||
|
||||
To clear the current user's data:
|
||||
|
||||
```js filename="index.js"
|
||||
useOpenPanel().clear()
|
||||
```
|
||||
|
||||
## Server side
|
||||
|
||||
If you want to track server-side events, you should create an instance of our Javascript SDK. It's exported from `@openpanel/nextjs`
|
||||
|
||||
<Callout>
|
||||
When using server events it's important that you use a secret to authenticate the request. This is to prevent unauthorized requests since we cannot use cors headers.
|
||||
|
||||
You can use the same clientId but you should pass the associated client secret to the SDK.
|
||||
|
||||
</Callout>
|
||||
|
||||
```typescript
|
||||
import { OpenpanelSdk } from '@openpanel/nextjs';
|
||||
|
||||
const opServer = new OpenpanelSdk({
|
||||
clientId: '{YOUR_CLIENT_ID}',
|
||||
clientSecret: '{YOUR_CLIENT_SECRET}',
|
||||
});
|
||||
|
||||
opServer.event('my_server_event', { ok: '✅' });
|
||||
|
||||
// Pass `profileId` to track events for a specific user
|
||||
opServer.event('my_server_event', { profileId: '123', ok: '✅' });
|
||||
```
|
||||
|
||||
### Serverless & Vercel
|
||||
|
||||
If you log events in a serverless environment like Vercel, you can use `waitUntil` to ensure the event is logged before the function is done.
|
||||
|
||||
Otherwise your function might close before the event is logged. Read more about it [here](https://vercel.com/docs/functions/functions-api-reference#waituntil).
|
||||
|
||||
```typescript
|
||||
import { waitUntil } from '@vercel/functions';
|
||||
import { opServer } from 'path/to/your-sdk-instance';
|
||||
|
||||
export function GET() {
|
||||
// Returns a response immediately while keeping the function alive
|
||||
waitUntil(opServer.event('my_server_event', { foo: 'bar' }));
|
||||
return new Response(`You're event has been logged!`);
|
||||
}
|
||||
```
|
||||
|
||||
### Proxy events
|
||||
|
||||
With `createNextRouteHandler` you can proxy your events through your server, this will ensure all events are tracked since there is a lot of adblockers that block requests to third party domains.
|
||||
|
||||
```typescript filename="/app/api/op/route.ts"
|
||||
import { createNextRouteHandler } from '@openpanel/nextjs/server';
|
||||
|
||||
export const POST = createNextRouteHandler();
|
||||
```
|
||||
|
||||
Remember to change the `apiUrl` in the `OpenPanelComponent` to your own server.
|
||||
|
||||
```tsx {2}
|
||||
<OpenPanelComponent
|
||||
apiUrl="/api/op"
|
||||
clientId="your-client-id"
|
||||
trackScreenViews={true}
|
||||
/>
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
# Node
|
||||
|
||||
Use <Link href="/docs/sdks/javascript">Javascript SDK</Link> to track events in Node.
|
||||
@@ -1,111 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
import { Callout, Steps, Tabs } from 'nextra/components';
|
||||
import { DeviceIdWarning } from 'src/components/device-id-warning';
|
||||
import { PersonalDataWarning } from 'src/components/personal-data-warning';
|
||||
|
||||
import CommonSdkConfig from 'src/components/common-sdk-config.mdx';
|
||||
|
||||
# React-Native
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
### Install dependencies
|
||||
|
||||
We're dependent on `expo-application` for `buildNumber`, `versionNumber` (and `referrer` on android) and `expo-constants` to get the `user-agent`.
|
||||
|
||||
```bash
|
||||
npm install @openpanel/react-native
|
||||
npx expo install expo-application expo-constants
|
||||
```
|
||||
|
||||
### Initialize
|
||||
|
||||
On native we use a clientSecret to authenticate the app.
|
||||
|
||||
```typescript
|
||||
const op = new Openpanel({
|
||||
clientId: '{YOUR_CLIENT_ID}',
|
||||
clientSecret: '{YOUR_CLIENT_SECRET}',
|
||||
});
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
<CommonSdkConfig />
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
### Track event
|
||||
|
||||
```typescript
|
||||
op.track('my_event', { foo: 'bar' });
|
||||
```
|
||||
|
||||
### Navigation / Screen views
|
||||
|
||||
<Tabs items={['expo-router', 'react-navigation (simple)']}>
|
||||
<Tabs.Tab>
|
||||
```typescript
|
||||
import { usePathname, useSegments } from 'expo-router';
|
||||
|
||||
function RootLayout() {
|
||||
// ...
|
||||
const pathname = usePathname()
|
||||
// Segments is optional but can be nice to have if you
|
||||
// want to group routes together
|
||||
// pathname = /posts/123
|
||||
// segements = ['posts', '[id]']
|
||||
const segments = useSegments()
|
||||
|
||||
useEffect(() => {
|
||||
// Simple
|
||||
op.screenView(pathname)
|
||||
|
||||
// With extra data
|
||||
op.screenView(pathname, {
|
||||
// segments is optional but nice to have
|
||||
segments: segments.join('/'),
|
||||
// other optional data you want to send with the screen view
|
||||
})
|
||||
}, [pathname,segments])
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab>
|
||||
```tsx
|
||||
import { createNavigationContainerRef } from '@react-navigation/native'
|
||||
import { Openpanel } from '@openpanel/react-native'
|
||||
|
||||
const op = new Openpanel({ /* ... */ })
|
||||
const navigationRef = createNavigationContainerRef()
|
||||
|
||||
export function NavigationRoot() {
|
||||
const handleNavigationStateChange = () => {
|
||||
const current = navigationRef.getCurrentRoute()
|
||||
if (current) {
|
||||
op.screenView(current.name, {
|
||||
params: current.params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationContainer
|
||||
ref={navigationRef}
|
||||
onReady={handleNavigationStateChange}
|
||||
onStateChange={handleNavigationStateChange}
|
||||
>
|
||||
<Stack.Navigator />
|
||||
</NavigationContainer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
|
||||
For more information on how to use the SDK, check out the [Javascript SDK](/docs/sdks/javascript#usage).
|
||||
@@ -1,5 +0,0 @@
|
||||
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.
|
||||
@@ -1,5 +0,0 @@
|
||||
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.
|
||||
@@ -1,193 +0,0 @@
|
||||
import { Callout, Tabs, Steps } from 'nextra/components';
|
||||
import { PersonalDataWarning } from 'src/components/personal-data-warning';
|
||||
import CommonSdkConfig from 'src/components/common-sdk-config.mdx';
|
||||
import WebSdkConfig from 'src/components/web-sdk-config.mdx';
|
||||
|
||||
# Web (Script Tag)
|
||||
|
||||
The OpenPanel Web SDK allows you to track user behavior on your website using a simple script tag. This guide provides instructions for installing and using the Web SDK in your project.
|
||||
|
||||
## Installation
|
||||
|
||||
Just insert this snippet and replace `YOUR_CLIENT_ID` with your client id.
|
||||
|
||||
```html filename="index.html" /clientId: 'YOUR_CLIENT_ID'/
|
||||
<script>
|
||||
window.op = window.op||function(...args){(window.op.q=window.op.q||[]).push(args);};
|
||||
window.op('init', {
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
</script>
|
||||
<script src="https://openpanel.dev/op1.js" defer async></script>
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
<CommonSdkConfig />
|
||||
<WebSdkConfig />
|
||||
|
||||
## Usage
|
||||
|
||||
### Tracking Events
|
||||
|
||||
You can track events with two different methods: by calling the `window.op('track')` directly or by adding `data-track` attributes to your HTML elements.
|
||||
|
||||
```html filename="index.html"
|
||||
<button type="button" onclick="window.op('track', 'my_event', { foo: 'bar' })">
|
||||
Track event
|
||||
</button>
|
||||
```
|
||||
|
||||
```html filename="index.html"
|
||||
<button type="button" data-track="my_event" data-foo="bar">Track event</button>
|
||||
```
|
||||
|
||||
### Identifying Users
|
||||
|
||||
To identify a user, call the `window.op('identify')` method with a unique identifier.
|
||||
|
||||
```js filename="main.js"
|
||||
window.op('identify', {
|
||||
profileId: '123', // Required
|
||||
firstName: 'Joe',
|
||||
lastName: 'Doe',
|
||||
email: 'joe@doe.com',
|
||||
properties: {
|
||||
tier: 'premium',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Setting Global Properties
|
||||
|
||||
To set properties that will be sent with every event:
|
||||
|
||||
```js filename="main.js"
|
||||
window.op('setGlobalProperties', {
|
||||
app_version: '1.0.2',
|
||||
environment: 'production',
|
||||
});
|
||||
```
|
||||
|
||||
### Creating Aliases
|
||||
|
||||
To create an alias for a user:
|
||||
|
||||
```js filename="main.js"
|
||||
window.op('alias', {
|
||||
alias: 'a1',
|
||||
profileId: '1'
|
||||
});
|
||||
```
|
||||
|
||||
### Incrementing Properties
|
||||
|
||||
To increment a numeric property on a user profile.
|
||||
|
||||
- `value` is the amount to increment the property by. If not provided, the property will be incremented by 1.
|
||||
|
||||
```js filename="main.js"
|
||||
window.op('increment', {
|
||||
profileId: '1',
|
||||
property: 'visits',
|
||||
value: 1 // optional
|
||||
});
|
||||
```
|
||||
|
||||
### Decrementing Properties
|
||||
|
||||
To decrement a numeric property on a user profile.
|
||||
|
||||
- `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1.
|
||||
|
||||
```js filename="main.js"
|
||||
window.op('decrement', {
|
||||
profileId: '1',
|
||||
property: 'visits',
|
||||
value: 1 // optional
|
||||
});
|
||||
```
|
||||
|
||||
### Clearing User Data
|
||||
|
||||
To clear the current user's data:
|
||||
|
||||
```js filename="main.js"
|
||||
window.op('clear');
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Using the Web SDK with NPM
|
||||
|
||||
<Steps>
|
||||
#### Step 1: Install the SDK
|
||||
|
||||
```bash
|
||||
npm install @openpanel/web
|
||||
```
|
||||
|
||||
#### Step 2: Initialize the SDK
|
||||
|
||||
```js filename="op.js"
|
||||
import { OpenPanel } from '@openpanel/web';
|
||||
|
||||
const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
```
|
||||
|
||||
#### Step 3: Use the SDK
|
||||
|
||||
```js filename="main.js"
|
||||
import { op } from './op.js';
|
||||
|
||||
op.track('my_event', { foo: 'bar' });
|
||||
```
|
||||
</Steps>
|
||||
|
||||
### Typescript
|
||||
|
||||
Getting ts errors when using the SDK? You can add a custom type definition file to your project.
|
||||
|
||||
#### Simple
|
||||
|
||||
Just paste this code in any of your `.d.ts` files.
|
||||
|
||||
```ts filename="op.d.ts"
|
||||
declare global {
|
||||
interface Window {
|
||||
op: {
|
||||
q?: string[][];
|
||||
(...args: [
|
||||
'init' | 'track' | 'identify' | 'setGlobalProperties' | 'alias' | 'increment' | 'decrement' | 'clear',
|
||||
...any[]
|
||||
]): void;
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Strict typing (from sdk)
|
||||
|
||||
<Steps>
|
||||
##### Step 1: Install the SDK
|
||||
|
||||
```bash
|
||||
npm install @openpanel/web
|
||||
```
|
||||
|
||||
##### Step 2: Create a type definition file
|
||||
|
||||
Create a `op.d.ts`file and paste the following code:
|
||||
|
||||
```ts filename="op.d.ts"
|
||||
/// <reference types="@openpanel/web" />
|
||||
```
|
||||
</Steps>
|
||||
@@ -1,5 +0,0 @@
|
||||
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.
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Callout, Tabs, Steps } from 'nextra/components';
|
||||
import { PersonalDataWarning } from 'src/components/personal-data-warning';
|
||||
import CommonSdkConfig from 'src/components/common-sdk-config.mdx';
|
||||
import WebSdkConfig from 'src/components/web-sdk-config.mdx';
|
||||
|
||||
# Web SDK
|
||||
|
||||
The OpenPanel Web SDK allows you to track user behavior on your website using a simple script tag. This guide provides instructions for installing and using the Web SDK in your project.
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
### Step 1: Install
|
||||
|
||||
```bash
|
||||
npm install @openpanel/web
|
||||
```
|
||||
|
||||
### Step 2: Initialize
|
||||
|
||||
```js filename="op.ts"
|
||||
import { OpenPanel } from '@openpanel/web';
|
||||
|
||||
const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
<CommonSdkConfig />
|
||||
<WebSdkConfig />
|
||||
|
||||
### Step 3: Usage
|
||||
|
||||
```js filename="main.ts"
|
||||
import { op } from './op.js';
|
||||
|
||||
op.track('my_event', { foo: 'bar' });
|
||||
```
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
Refer to the [Javascript SDK](/docs/sdks/javascript#usage) for usage instructions.
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"index": "Get started"
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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)
|
||||
- 🙋♂️ This should work on any system if you have pre-installed docker, node and pnpm
|
||||
- [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 && ./setup
|
||||
# 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 1-2 minutes depending on your VPS
|
||||
|
||||
```bash
|
||||
cd openpanel/self-hosting
|
||||
./setup
|
||||
```
|
||||
|
||||
⚠️ If the `./setup` script fails to run, you can do it manually.
|
||||
|
||||
1. Install docker
|
||||
2. Install node
|
||||
3. Install pnpm
|
||||
4. Run the `npx jiti ./quiz.ts` script inside the self-hosting folder
|
||||
|
||||
### 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,
|
||||
});
|
||||
```
|
||||
|
||||
### Managed Redis
|
||||
|
||||
If you use a managed Redis service, you may need to set the `notify-keyspace-events` manually.
|
||||
|
||||
Without this setting we wont be able to listen for expired keys which we use for caluclating currently active vistors.
|
||||
|
||||
> You will see a warning in the logs if this needs to be set manually.
|
||||
@@ -1,16 +0,0 @@
|
||||
<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.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Privacy-focused**: Openpanel is built with privacy in mind. We don't track any personal data and we don't use cookies.
|
||||
- ✅ **Open-source**: Openpanel is open-source and you can host it yourself.
|
||||
- ✅ **Cloud-hosted**: You can choose our cloud hosting if you don't want to host it yourself. We take care of the infrastructure and you can focus on your business.
|
||||
- ✅ **Real-time analytics**: Everything is updated in real-time, no delays to see your insights.
|
||||
- ✅ **Event-based tracking**: You can track any event you want, and you can track as many events as you want.
|
||||
- ✅ **Custom properties**: You can add custom properties to your events to track more data.
|
||||
- ✅ **Funnel analysis**: You can create funnels to see how your users are interacting with your product.
|
||||
- ✅ **Reports**: Create as many reports as you want to visualize the data you need.
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useConfig } from 'nextra-theme-docs';
|
||||
|
||||
export default {
|
||||
banner: {
|
||||
key: '1.0-release',
|
||||
text: (
|
||||
<a href="/docs/migration/beta-v1">
|
||||
🎉 We have released v1. Read migration guide if needed!
|
||||
</a>
|
||||
),
|
||||
},
|
||||
logo: (
|
||||
<>
|
||||
<Image
|
||||
src="https://dashboard.openpanel.dev/logo.svg"
|
||||
alt="next-international logo"
|
||||
height="32"
|
||||
width="32"
|
||||
/>
|
||||
<strong style={{ marginLeft: '8px' }}>OpenPanel</strong>
|
||||
</>
|
||||
),
|
||||
head: () => {
|
||||
const router = useRouter();
|
||||
const config = useConfig();
|
||||
const title = config.title;
|
||||
const description = 'An open-source alternative to Mixpanel';
|
||||
const domain = 'https://docs.openpanel.dev';
|
||||
const canonicalUrl =
|
||||
`${domain}${router.asPath === '/' ? '' : router.asPath}`.split('?')[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta property="description" content={description} />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:site:domain"
|
||||
content={domain.replace('https://', '')}
|
||||
/>
|
||||
<meta name="twitter:url" content={domain} />
|
||||
<meta name="og:type" content={'site'} />
|
||||
<meta name="og:url" content={canonicalUrl} />
|
||||
<meta name="og:title" content={`${title} - Openpanel Docs`} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta
|
||||
name="og:image"
|
||||
content={'https://docs.openpanel.dev/ogimage.png'}
|
||||
/>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
useNextSeoProps() {
|
||||
return {
|
||||
titleTemplate: '%s - Openpanel Docs',
|
||||
};
|
||||
},
|
||||
search: {
|
||||
placeholder: 'Search',
|
||||
},
|
||||
project: {
|
||||
link: 'https://github.com/openpanel-dev/openpanel',
|
||||
},
|
||||
docsRepositoryBase:
|
||||
'https://github.com/openpanel-dev/openpanel/blob/main/apps/docs',
|
||||
footer: {
|
||||
text: (
|
||||
<span>
|
||||
Made with ❤️ by{' '}
|
||||
<a
|
||||
href="https://x.com/OpenPanelDev"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
>
|
||||
Carl
|
||||
</a>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"extends": "@openpanel/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [".", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user