diff --git a/.vscode/settings.json b/.vscode/settings.json index cfc509c..7aa6142 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + "editor.tabSize": 2, "editor.formatOnSave": true, "editor.tabCompletion": "on", "[typescript]": { diff --git a/examples/next-with-approuter/next.config.js b/examples/next-with-approuter/next.config.js index 767719f..56d2cc7 100644 --- a/examples/next-with-approuter/next.config.js +++ b/examples/next-with-approuter/next.config.js @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instrumentationHook: true, + }, +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/examples/next-with-approuter/package.json b/examples/next-with-approuter/package.json index e42b907..a0d0291 100644 --- a/examples/next-with-approuter/package.json +++ b/examples/next-with-approuter/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 4000", "build": "next build", "start": "next start", "lint": "next lint" @@ -16,6 +16,6 @@ "react": "18.2.0", "react-dom": "18.2.0", "typescript": "5.2.2", - "@aptabase/next": "*" + "@aptabase/nextjs": "*" } } diff --git a/examples/next-with-approuter/src/app/Counter.tsx b/examples/next-with-approuter/src/app/Counter.tsx new file mode 100644 index 0000000..cdac6dd --- /dev/null +++ b/examples/next-with-approuter/src/app/Counter.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { useAptabase } from '@aptabase/nextjs'; +import { useState } from 'react'; + +export function Counter() { + const { trackEvent } = useAptabase(); + const [count, setCount] = useState(0); + + function increment() { + setCount((c) => c + 1); + trackEvent('increment'); + } + + function decrement() { + setCount((c) => c - 1); + trackEvent('decrement'); + } + + return ( +
+

Count: {count}

+ + +
+ ); +} diff --git a/examples/next-with-approuter/src/app/layout.tsx b/examples/next-with-approuter/src/app/layout.tsx index dbce4ea..3d5895e 100644 --- a/examples/next-with-approuter/src/app/layout.tsx +++ b/examples/next-with-approuter/src/app/layout.tsx @@ -1,11 +1,11 @@ -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { +import { AptabaseProvider } from '@aptabase/nextjs'; + +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} + - ) + ); } diff --git a/examples/next-with-approuter/src/app/page.tsx b/examples/next-with-approuter/src/app/page.tsx index c853c44..86d95df 100644 --- a/examples/next-with-approuter/src/app/page.tsx +++ b/examples/next-with-approuter/src/app/page.tsx @@ -1,9 +1,11 @@ -import { getName } from '@aptabase/next' +import { trackEvent } from '@aptabase/nextjs/server'; +import { Counter } from './Counter'; -export default function Home() { +export default async function Home() { + await trackEvent('page_view', { page: 'home' }); return (
- Hello from Next.js App Router: {getName()} + Hello from Next.js App Router
- ) + ); } diff --git a/examples/next-with-approuter/src/instrumentation.ts b/examples/next-with-approuter/src/instrumentation.ts new file mode 100644 index 0000000..9de5fce --- /dev/null +++ b/examples/next-with-approuter/src/instrumentation.ts @@ -0,0 +1,5 @@ +import { init } from '@aptabase/nextjs'; + +export function register() { + init('A-DEV-0000000000'); +} diff --git a/package-lock.json b/package-lock.json index 3341b9a..c99fbdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "examples/next-with-approuter": { "version": "0.1.0", "dependencies": { - "@aptabase/next": "*", + "@aptabase/nextjs": "*", "@types/node": "20.5.7", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", @@ -33,8 +33,8 @@ "typescript": "5.2.2" } }, - "node_modules/@aptabase/next": { - "resolved": "packages/next", + "node_modules/@aptabase/nextjs": { + "resolved": "packages/nextjs", "link": true }, "node_modules/@aptabase/web": { @@ -989,6 +989,7 @@ "packages/next": { "name": "@aptabase/next", "version": "0.0.1", + "extraneous": true, "license": "MIT", "devDependencies": { "@rollup/plugin-replace": "5.0.2", @@ -999,7 +1000,24 @@ "typescript": "5.2.2" } }, - "packages/next/node_modules/rollup": { + "packages/nextjs": { + "name": "@aptabase/nextjs", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-replace": "5.0.2", + "@rollup/plugin-terser": "0.4.3", + "@rollup/plugin-typescript": "11.1.3", + "rollup": "3.28.1", + "tslib": "2.6.2", + "typescript": "5.2.2" + }, + "peerDependencies": { + "next": "^13.0.0", + "react": "^18.0.0" + } + }, + "packages/nextjs/node_modules/rollup": { "version": "3.28.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", @@ -1015,7 +1033,7 @@ "fsevents": "~2.3.2" } }, - "packages/next/node_modules/tslib": { + "packages/nextjs/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", diff --git a/package.json b/package.json index 2e5d148..f708d49 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ ], "scripts": { "build": "turbo run build", - "watch": "turbo run watch --parallel", + "build-packages": "turbo run build --filter=./packages/*", "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "devDependencies": { diff --git a/packages/next/rollup.config.mjs b/packages/next/rollup.config.mjs deleted file mode 100644 index b466a96..0000000 --- a/packages/next/rollup.config.mjs +++ /dev/null @@ -1,38 +0,0 @@ -import replace from '@rollup/plugin-replace'; -import terser from '@rollup/plugin-terser'; -import typescript from '@rollup/plugin-typescript'; -import pkg from './package.json' assert { type: 'json' }; - -const plugins = [ - terser(), - replace({ - 'env.PKG_VERSION': pkg.version, - preventAssignment: true, - }), - typescript({ - tsconfig: './tsconfig.json', - moduleResolution: 'node', - }), -]; - -const cjs = { - input: './src/index.ts', - output: { - dir: './dist', - entryFileNames: '[name].js', - format: 'cjs', - }, - plugins, -}; - -const es = { - input: './src/index.ts', - output: { - dir: './dist', - entryFileNames: '[name].mjs', - format: 'es', - }, - plugins, -}; - -export default [cjs, es]; diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts deleted file mode 100644 index 0eab8ce..0000000 --- a/packages/next/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getName() { - return "next" -} \ No newline at end of file diff --git a/packages/next/tsconfig.json b/packages/next/tsconfig.json deleted file mode 100644 index 28c5fae..0000000 --- a/packages/next/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES5", - "strict": true, - "allowJs": true, - "esModuleInterop": true, - "baseUrl": ".", - "paths": { - "types": ["@types"] - }, - "declaration": true, - "declarationDir": "./dist", - "rootDir": "./src" - }, - "include": ["./"] -} diff --git a/packages/next/package.json b/packages/nextjs/package.json similarity index 66% rename from packages/next/package.json rename to packages/nextjs/package.json index a0c1859..0587992 100644 --- a/packages/next/package.json +++ b/packages/nextjs/package.json @@ -1,23 +1,23 @@ { - "name": "@aptabase/next", + "name": "@aptabase/nextjs", "version": "0.0.1", - "private": false, "type": "module", "description": "Next.js SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps", - "main": "./dist/index.js", - "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "exports": { ".": { - "require": "./dist/index.js", - "import": "./dist/index.mjs", - "types": "./dist/index.d.ts" + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./server": { + "types": "./dist/server.d.ts", + "default": "./dist/server.js" } }, "repository": { "type": "git", "url": "git+https://github.com/aptabase/aptabase-js.git", - "directory": "packages/js" + "directory": "packages/nextjs" }, "bugs": { "url": "https://github.com/aptabase/aptabase-js/issues" @@ -25,10 +25,7 @@ "homepage": "https://github.com/aptabase/aptabase-js", "license": "MIT", "scripts": { - "build": "rollup -c ./rollup.config.mjs", - "watch": "rollup -c ./rollup.config.mjs -w", - "prepublishOnly": "npm run build", - "pretest": "npm run build" + "build": "tsc" }, "files": [ "README.md", @@ -43,5 +40,9 @@ "@rollup/plugin-terser": "0.4.3", "tslib": "2.6.2", "typescript": "5.2.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "next": "^13.0.0" } } diff --git a/packages/nextjs/src/client.tsx b/packages/nextjs/src/client.tsx new file mode 100644 index 0000000..1920f0c --- /dev/null +++ b/packages/nextjs/src/client.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { createContext, useContext } from 'react'; +import { AptabaseOptions } from './types'; + +type TrackEventFn = (eventName: string, props?: Record) => Promise; + +type ContextProps = { + appKey?: string; + client?: AptabaseClient; +} & AptabaseOptions; + +export type AptabaseClient = { + trackEvent: TrackEventFn; +}; + +const AptabaseContext = createContext({}); + +type Props = { + appKey: string; + options?: AptabaseOptions; + children: React.ReactNode; +}; + +export function AptabaseProvider({ appKey, options, children }: Props) { + const client: AptabaseClient = { + trackEvent: (eventName, props) => sendEvent(appKey, eventName, props), + }; + + return {children}; +} + +export function useAptabase(): AptabaseClient { + const ctx = useContext(AptabaseContext); + if (!ctx.client) { + throw new Error( + 'useAptabase must be used within AptabaseProvider. Did you forget to wrap your app in ?', + ); + } + + return ctx.client; +} + +async function sendEvent( + appKey: string | undefined, + eventName: string, + props?: Record, +): Promise { + if (!appKey) return Promise.resolve(); + + const body = JSON.stringify({ + timestamp: new Date().toISOString(), + sessionId: 'CHANGE-THIS', + eventName: eventName, + systemProps: { + isDebug: true, + locale: 'en', + appVersion: '', + sdkVersion: 'aptabase-nextjs@0.0.1', + }, + props: props, + }); + + try { + const response = await fetch('http://localhost:3000/api/v0/event', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'App-Key': appKey, + }, + credentials: 'omit', + body, + }); + + if (response.status >= 300) { + const responseBody = await response.text(); + console.warn(`Failed to send event "${eventName}": ${response.status} ${responseBody}`); + } + } catch (e) { + console.warn(`Failed to send event "${eventName}": ${e}`); + } +} diff --git a/packages/nextjs/src/global.d.ts b/packages/nextjs/src/global.d.ts new file mode 100644 index 0000000..ecaf0f1 --- /dev/null +++ b/packages/nextjs/src/global.d.ts @@ -0,0 +1,5 @@ +type AptabaseState = { + appKey?: string; +}; + +declare var __APTABASE__: AptabaseState; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts new file mode 100644 index 0000000..d3df734 --- /dev/null +++ b/packages/nextjs/src/index.ts @@ -0,0 +1,5 @@ +export * from './client'; + +export function init(appKey: string) { + globalThis.__APTABASE__ = { appKey }; +} diff --git a/packages/nextjs/src/server.ts b/packages/nextjs/src/server.ts new file mode 100644 index 0000000..1bbb563 --- /dev/null +++ b/packages/nextjs/src/server.ts @@ -0,0 +1,39 @@ +import { headers } from 'next/headers'; + +export async function trackEvent(eventName: string, props?: Record): Promise { + const appKey = globalThis.__APTABASE__.appKey; + if (!appKey) return Promise.resolve(); + + const body = JSON.stringify({ + timestamp: new Date().toISOString(), + sessionId: 'CHANGE-THIS', + eventName: eventName, + systemProps: { + isDebug: true, + locale: 'en', + appVersion: '', + sdkVersion: 'aptabase-nextjs@0.0.1', + }, + props: props, + }); + + try { + const response = await fetch('http://localhost:3000/api/v0/event', { + method: 'POST', + headers: { + 'User-Agent': headers().get('User-Agent') ?? '', + 'Content-Type': 'application/json', + 'App-Key': appKey, + }, + credentials: 'omit', + body, + }); + + if (response.status >= 300) { + const responseBody = await response.text(); + console.warn(`Failed to send event "${eventName}": ${response.status} ${responseBody}`); + } + } catch (e) { + console.warn(`Failed to send event "${eventName}": ${e}`); + } +} diff --git a/packages/nextjs/src/types.ts b/packages/nextjs/src/types.ts new file mode 100644 index 0000000..e8835a2 --- /dev/null +++ b/packages/nextjs/src/types.ts @@ -0,0 +1,4 @@ +export type AptabaseOptions = { + host?: string; + appVersion?: string; +}; diff --git a/packages/nextjs/tsconfig.json b/packages/nextjs/tsconfig.json new file mode 100644 index 0000000..00f5a87 --- /dev/null +++ b/packages/nextjs/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "module": "esnext", + "jsx": "react-jsx", + "moduleResolution": "node", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist", + "sourceMap": true, + "strict": true, + "target": "es2020", + "types": ["node"], + }, + "include": ["src/*", "src/global.d.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/web/package.json b/packages/web/package.json index dd7e481..2da99ec 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,6 @@ { "name": "@aptabase/web", "version": "0.1.3", - "private": false, "type": "module", "description": "JavaScript SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps", "main": "./dist/index.js", @@ -17,7 +16,7 @@ "repository": { "type": "git", "url": "git+https://github.com/aptabase/aptabase-js.git", - "directory": "packages/js" + "directory": "packages/web" }, "bugs": { "url": "https://github.com/aptabase/aptabase-js/issues" diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json index 28c5fae..0f249c6 100644 --- a/packages/web/tsconfig.json +++ b/packages/web/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { "target": "ES5", "strict": true, - "allowJs": true, "esModuleInterop": true, "baseUrl": ".", "paths": {