This commit is contained in:
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["next-with-approuter", "next-with-pages", "plasmo-browserext", "remix-example"]
|
||||
}
|
||||
4
.eslintrc.js
Normal file
4
.eslintrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['eslint:recommended', 'prettier'],
|
||||
};
|
||||
35
.github/workflows/release.yaml
vendored
Normal file
35
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
publish: npm run ci:publish
|
||||
commit: 'chore: update versions'
|
||||
title: 'chore: update versions'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.cache
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# turbo
|
||||
.turbo
|
||||
|
||||
# bundler output
|
||||
dist
|
||||
build
|
||||
.next
|
||||
.plasmo
|
||||
|
||||
# misc
|
||||
.angular
|
||||
.storybook/documentation.json
|
||||
.sass-cache
|
||||
connect.lock
|
||||
libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# shared file copied in angular lib folder
|
||||
/packages/angular/aptabase-angular/src/shared.ts
|
||||
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.tabCompletion": "on",
|
||||
"[typescript]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"[typescript][typescriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
README.md
Normal file
8
README.md
Normal file
@@ -0,0 +1,8 @@
|
||||

|
||||
|
||||
This repository hosts the code for:
|
||||
|
||||
- [SDK for Web Apps](./packages/web/README.md)
|
||||
- [SDK for Browser Extensions](./packages/browser/README.md)
|
||||
- [SDK for React / Remix / Next.js](./packages/react/README.md)
|
||||
- [SDK for Angular](./packages/angular/README.md)
|
||||
5
examples/next-with-approuter/next-env.d.ts
vendored
Normal file
5
examples/next-with-approuter/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
6
examples/next-with-approuter/next.config.js
Normal file
6
examples/next-with-approuter/next.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
21
examples/next-with-approuter/package.json
Normal file
21
examples/next-with-approuter/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "next-with-approuter",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 4000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"next": "13.4.19",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"typescript": "5.2.2",
|
||||
"@aptabase/react": "*"
|
||||
}
|
||||
}
|
||||
27
examples/next-with-approuter/src/app/Counter.tsx
Normal file
27
examples/next-with-approuter/src/app/Counter.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useAptabase } from '@aptabase/react';
|
||||
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 (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
<button onClick={decrement}>Decrement</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
examples/next-with-approuter/src/app/layout.tsx
Normal file
11
examples/next-with-approuter/src/app/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AptabaseProvider } from '@aptabase/react';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AptabaseProvider appKey="A-US-5431775171">{children}</AptabaseProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
9
examples/next-with-approuter/src/app/page.tsx
Normal file
9
examples/next-with-approuter/src/app/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Counter } from './Counter';
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<main>
|
||||
Aptabase + Next.js App Router <br /> <Counter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
27
examples/next-with-approuter/tsconfig.json
Normal file
27
examples/next-with-approuter/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
5
examples/next-with-pages/next-env.d.ts
vendored
Normal file
5
examples/next-with-pages/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
7
examples/next-with-pages/next.config.js
Normal file
7
examples/next-with-pages/next.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
21
examples/next-with-pages/package.json
Normal file
21
examples/next-with-pages/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "next-with-pages",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 4000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"next": "13.4.19",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"typescript": "5.2.2",
|
||||
"@aptabase/react": "*"
|
||||
}
|
||||
}
|
||||
25
examples/next-with-pages/src/components/Counter.tsx
Normal file
25
examples/next-with-pages/src/components/Counter.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useAptabase } from '@aptabase/react';
|
||||
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 (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
<button onClick={decrement}>Decrement</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
examples/next-with-pages/src/pages/_app.tsx
Normal file
10
examples/next-with-pages/src/pages/_app.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { AptabaseProvider } from '@aptabase/react';
|
||||
import type { AppProps } from 'next/app';
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<AptabaseProvider appKey="A-DEV-0000000000">
|
||||
<Component {...pageProps} />
|
||||
</AptabaseProvider>
|
||||
);
|
||||
}
|
||||
13
examples/next-with-pages/src/pages/_document.tsx
Normal file
13
examples/next-with-pages/src/pages/_document.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
9
examples/next-with-pages/src/pages/api/hello.ts
Normal file
9
examples/next-with-pages/src/pages/api/hello.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
type Data = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
||||
res.status(200).json({ name: 'John Doe' });
|
||||
}
|
||||
23
examples/next-with-pages/src/pages/index.tsx
Normal file
23
examples/next-with-pages/src/pages/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Counter } from '@/components/Counter';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<main>
|
||||
Aptabase + Next.js Pages Router <br />
|
||||
<Counter />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
35
examples/next-with-pages/tsconfig.json
Normal file
35
examples/next-with-pages/tsconfig.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
BIN
examples/plasmo-browserext/assets/icon.png
Normal file
BIN
examples/plasmo-browserext/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
5
examples/plasmo-browserext/background.ts
Normal file
5
examples/plasmo-browserext/background.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { init, trackEvent } from '@aptabase/browser';
|
||||
|
||||
init('A-DEV-0000000000');
|
||||
|
||||
trackEvent('background_service_started');
|
||||
33
examples/plasmo-browserext/package.json
Normal file
33
examples/plasmo-browserext/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "plasmo-browserext",
|
||||
"private": true,
|
||||
"displayName": "Plasmo browserext",
|
||||
"version": "0.0.1",
|
||||
"description": "A basic Plasmo extension.",
|
||||
"author": "Plasmo Corp. <foss@plasmo.com>",
|
||||
"scripts": {
|
||||
"dev": "plasmo dev",
|
||||
"build": "plasmo build",
|
||||
"test": "plasmo test"
|
||||
},
|
||||
"dependencies": {
|
||||
"plasmo": "0.84.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@aptabase/browser": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "0.0.254",
|
||||
"@types/react": "18.2.45",
|
||||
"@types/react-dom": "18.2.17",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"manifest": {
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://*/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
25
examples/plasmo-browserext/popup.tsx
Normal file
25
examples/plasmo-browserext/popup.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { trackEvent } from '@aptabase/browser';
|
||||
import { useState } from 'react';
|
||||
|
||||
function IndexPopup() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
function increment() {
|
||||
trackEvent('increment', { count: count + 1 });
|
||||
setCount((c) => c + 1);
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
trackEvent('decrement', { count: count - 1 });
|
||||
setCount((c) => c - 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={increment}>Increment</button>
|
||||
<button onClick={decrement}>decrement</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexPopup;
|
||||
11
examples/plasmo-browserext/tsconfig.json
Normal file
11
examples/plasmo-browserext/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "plasmo/templates/tsconfig.base",
|
||||
"exclude": ["node_modules"],
|
||||
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"~*": ["./*"]
|
||||
},
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
25
examples/remix-app/app/components/Counter.tsx
Normal file
25
examples/remix-app/app/components/Counter.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useAptabase } from '@aptabase/react';
|
||||
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 (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
<button onClick={decrement}>Decrement</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
examples/remix-app/app/entry.client.tsx
Normal file
21
examples/remix-app/app/entry.client.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* By default, Remix will handle hydrating your app on the client for you.
|
||||
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
||||
* For more information, see https://remix.run/file-conventions/entry.client
|
||||
*/
|
||||
|
||||
import { AptabaseProvider } from '@aptabase/react';
|
||||
import { RemixBrowser } from '@remix-run/react';
|
||||
import { startTransition, StrictMode } from 'react';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<AptabaseProvider appKey="A-DEV-0000000000">
|
||||
<RemixBrowser />
|
||||
</AptabaseProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
135
examples/remix-app/app/entry.server.tsx
Normal file
135
examples/remix-app/app/entry.server.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* By default, Remix will handle generating the HTTP Response for you.
|
||||
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
||||
* For more information, see https://remix.run/file-conventions/entry.server
|
||||
*/
|
||||
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
import type { AppLoadContext, EntryContext } from "@remix-run/node";
|
||||
import { Response } from "@remix-run/node";
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import isbot from "isbot";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
|
||||
const ABORT_DELAY = 5_000;
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
loadContext: AppLoadContext
|
||||
) {
|
||||
return isbot(request.headers.get("user-agent"))
|
||||
? handleBotRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
)
|
||||
: handleBrowserRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
);
|
||||
}
|
||||
|
||||
function handleBotRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
{
|
||||
onAllReady() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
resolve(
|
||||
new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
function handleBrowserRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
{
|
||||
onShellReady() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
resolve(
|
||||
new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
||||
33
examples/remix-app/app/root.tsx
Normal file
33
examples/remix-app/app/root.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { cssBundleHref } from "@remix-run/css-bundle";
|
||||
import type { LinksFunction } from "@remix-run/node";
|
||||
import {
|
||||
Links,
|
||||
LiveReload,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
|
||||
];
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
9
examples/remix-app/app/routes/_index.tsx
Normal file
9
examples/remix-app/app/routes/_index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Counter } from '~/components/Counter';
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<main>
|
||||
Aptabase + Remix <br /> <Counter />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
30
examples/remix-app/package.json
Normal file
30
examples/remix-app/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "remix-example",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "remix build",
|
||||
"dev": "PORT=4000 remix dev",
|
||||
"start": "remix-serve build",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/css-bundle": "1.19.3",
|
||||
"@remix-run/node": "1.19.3",
|
||||
"@remix-run/react": "1.19.3",
|
||||
"@remix-run/serve": "1.19.3",
|
||||
"isbot": "3.6.8",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@aptabase/react": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "1.19.3",
|
||||
"@remix-run/eslint-config": "1.19.3",
|
||||
"@types/react": "18.0.35",
|
||||
"@types/react-dom": "18.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
}
|
||||
17
examples/remix-app/remix.config.js
Normal file
17
examples/remix-app/remix.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/** @type {import('@remix-run/dev').AppConfig} */
|
||||
module.exports = {
|
||||
ignoredRouteFiles: ["**/.*"],
|
||||
// appDirectory: "app",
|
||||
// assetsBuildDirectory: "public/build",
|
||||
// serverBuildPath: "build/index.js",
|
||||
// publicPath: "/build/",
|
||||
serverModuleFormat: "cjs",
|
||||
future: {
|
||||
v2_dev: true,
|
||||
v2_errorBoundary: true,
|
||||
v2_headers: true,
|
||||
v2_meta: true,
|
||||
v2_normalizeFormMethod: true,
|
||||
v2_routeConvention: true,
|
||||
},
|
||||
};
|
||||
2
examples/remix-app/remix.env.d.ts
vendored
Normal file
2
examples/remix-app/remix.env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="@remix-run/dev" />
|
||||
/// <reference types="@remix-run/node" />
|
||||
22
examples/remix-app/tsconfig.json
Normal file
22
examples/remix-app/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2019",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
|
||||
// Remix takes care of building everything in `remix build`.
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
90
examples/simple-js/JS_load.md
Normal file
90
examples/simple-js/JS_load.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 정적 JS 파일 빌드 및 문제 해결 가이드
|
||||
|
||||
이 문서는 `aptabase.min.js`와 `aptabase.debug.js` 파일을 빌드하고, 빌드 과정에서 발생한 문제들을 해결하는 과정을 기록합니다.
|
||||
|
||||
## 초기화 및 이벤트 전송
|
||||
|
||||
### 기본 사용법
|
||||
|
||||
`trackEvent` 함수를 사용하여 이벤트를 전송할 때, 두 번째 인자로 JavaScript 객체를 전달하여 사용자 정의 속성(`props`)을 함께 보낼 수 있습니다. `props` 객체의 값으로는 문자열, 숫자, 불리언 타입이 가능합니다.
|
||||
|
||||
**예제:**
|
||||
|
||||
```html
|
||||
<script>
|
||||
// Aptabase 초기화
|
||||
window.aptabase.init("YOUR_APP_KEY");
|
||||
|
||||
function track(eventName, props) {
|
||||
// 이벤트와 함께 props 전송
|
||||
window.aptabase.trackEvent(eventName, props);
|
||||
console.log(`Event '${eventName}' sent!`, props ? { props } : '');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- props 없이 이벤트 전송 -->
|
||||
<button onclick="track('app_started')">Track: App Started</button>
|
||||
|
||||
<!-- props와 함께 이벤트 전송 -->
|
||||
<button onclick="track('feature_a_used', { plan: 'premium', from: 'sidebar' })">Track: Used Feature A</button>
|
||||
<button onclick="track('item_purchased', { item_id: 'SKU-123', price: 9.99, on_sale: true })">Track: Item Purchased</button>
|
||||
```
|
||||
|
||||
### Self-Hosted 환경 초기화
|
||||
|
||||
자체 호스팅(Self-hosting) 환경의 Aptabase를 사용하는 경우, `init` 함수의 두 번째 인자로 `host` 옵션을 반드시 전달해야 합니다. Self-hosted용 App Key는 일반적으로 `SH` 지역 코드를 포함합니다.
|
||||
|
||||
**예제:**
|
||||
```html
|
||||
<script>
|
||||
// Self-Hosted용 App Key와 host 주소를 사용하여 초기화
|
||||
window.aptabase.init("A-SH-YOUR_UNIQUE_ID", {
|
||||
host: "https://your-aptabase-instance.com"
|
||||
});
|
||||
|
||||
function track(eventName, props) {
|
||||
window.aptabase.trackEvent(eventName, props);
|
||||
console.log(`Event '${eventName}' sent!`);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 빌드 문제 해결 과정
|
||||
|
||||
### 1. `tsup` 빌드 설정 수정
|
||||
|
||||
- **문제**: 초기 `tsup.config.ts` 설정은 `aptabase.min.js`와 `aptabase.debug.js`를 명시적으로 생성하지 않아 minify 과정에서 문제가 발생할 가능성이 있었습니다.
|
||||
- **해결**: `tsup.config.ts` 파일을 수정하여 디버그와 minify 버전을 각각 다른 `define` 설정을 통해 생성하도록 빌드 구성을 분리했습니다.
|
||||
|
||||
### 2. `pnpm` 워크스페이스 설정
|
||||
|
||||
- **문제**: `pnpm`을 사용하는 모노레포 환경에서 `pnpm install`을 실행했음에도 `tsup` 명령어를 찾지 못하는 문제가 발생했습니다.
|
||||
- **원인**: `pnpm-workspace.yaml` 파일이 없어 `pnpm`이 워크스페이스를 올바르게 인식하지 못했습니다.
|
||||
- **해결**: 프로젝트 루트에 `pnpm-workspace.yaml` 파일을 생성하고 `packages`와 `examples` 디렉토리를 워크스페이스로 지정한 후, `pnpm install`을 다시 실행하여 의존성을 올바르게 설치했습니다.
|
||||
|
||||
### 3. 타입 정의(Typings) 문제 해결
|
||||
|
||||
- **문제**: 빌드 중 `Cannot find name 'chrome'` 및 `Cannot find name 'process'`와 같은 타입스크립트 컴파일 오류가 발생했습니다.
|
||||
- **원인**: 브라우저 및 Node.js 환경에서만 제공되는 전역 객체(`chrome`, `process`)에 대한 타입 정의가 없었습니다.
|
||||
- **해결**:
|
||||
1. `@types/chrome`과 `@types/node` 패키지를 개발 의존성으로 설치했습니다.
|
||||
2. `packages/web/tsconfig.json`의 `compilerOptions.types` 배열에 `"chrome"`과 `"node"`를 추가하여 타입스크립트가 해당 타입들을 인식하도록 설정했습니다.
|
||||
|
||||
### 4. `process` is not defined` 런타임 오류 해결
|
||||
|
||||
- **문제**: 빌드된 `aptabase.min.js` 파일을 브라우저에서 실행할 때 `ReferenceError: process is not defined` 런타임 오류가 발생했습니다.
|
||||
- **원인**: 소스코드(`shared.ts`, `index.ts`)에서 `process.env.NODE_ENV`와 `process.env.PKG_VERSION`을 참조하고 있었고, 이 코드가 브라우저 환경에서 실행<EC8BA4><ED9689>면서 `process` 객체를 찾지 못해 오류가 발생했습니다.
|
||||
- **해결**: `tsup.config.ts`의 `define` 옵션을 사용하여 빌드 시점에 해당 환경 변수들을 실제 값으로 교체하도록 설정했습니다.
|
||||
- `aptabase.min.js` (프로덕션용): `process.env.NODE_ENV`를 `'production'`으로 설정
|
||||
- `aptabase.debug.js` (디버그용): `process.env.NODE_ENV`를 `'development'`로 설정
|
||||
|
||||
## 최종 빌드 방법
|
||||
|
||||
위의 모든 문제 해결 후, 다음 명령어를 사용하여 성공적으로 `aptabase.min.js`와 `aptabase.debug.js` 파일을 빌드할 수 있습니다.
|
||||
|
||||
```bash
|
||||
# 프로젝트 루트 디렉토리에서 실행
|
||||
pnpm --filter @aptabase/web build
|
||||
```
|
||||
175
examples/simple-js/aptabase.debug.js
Normal file
175
examples/simple-js/aptabase.debug.js
Normal file
@@ -0,0 +1,175 @@
|
||||
"use strict";
|
||||
var aptabase = (() => {
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
init: () => init,
|
||||
trackEvent: () => trackEvent
|
||||
});
|
||||
|
||||
// ../shared.ts
|
||||
var defaultLocale;
|
||||
var defaultIsDebug;
|
||||
var isInBrowser = typeof window !== "undefined" && typeof window.fetch !== "undefined";
|
||||
var isInBrowserExtension = typeof chrome !== "undefined" && !!chrome.runtime?.id;
|
||||
var _sessionId = newSessionId();
|
||||
var _lastTouched = /* @__PURE__ */ new Date();
|
||||
var _hosts = {
|
||||
US: "https://us.aptabase.com",
|
||||
EU: "https://eu.aptabase.com",
|
||||
DEV: "https://localhost:3000",
|
||||
SH: ""
|
||||
};
|
||||
function inMemorySessionId(timeout) {
|
||||
let now = /* @__PURE__ */ new Date();
|
||||
const diffInMs = now.getTime() - _lastTouched.getTime();
|
||||
const diffInSec = Math.floor(diffInMs / 1e3);
|
||||
if (diffInSec > timeout) {
|
||||
_sessionId = newSessionId();
|
||||
}
|
||||
_lastTouched = now;
|
||||
return _sessionId;
|
||||
}
|
||||
function newSessionId() {
|
||||
const epochInSeconds = Math.floor(Date.now() / 1e3).toString();
|
||||
const random = Math.floor(Math.random() * 1e8).toString().padStart(8, "0");
|
||||
return epochInSeconds + random;
|
||||
}
|
||||
function validateAppKey(appKey) {
|
||||
const parts = appKey.split("-");
|
||||
if (parts.length !== 3 || _hosts[parts[1]] === void 0) {
|
||||
console.warn(`The Aptabase App Key "${appKey}" is invalid. Tracking will be disabled.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function getApiUrl(appKey, options) {
|
||||
const region = appKey.split("-")[1];
|
||||
if (region === "SH") {
|
||||
if (!options?.host) {
|
||||
console.warn(`Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.`);
|
||||
return;
|
||||
}
|
||||
return `${options.host}/api/v0/event`;
|
||||
}
|
||||
const host = options?.host ?? _hosts[region];
|
||||
return `${host}/api/v0/event`;
|
||||
}
|
||||
async function sendEvent(opts) {
|
||||
if (!isInBrowser && !isInBrowserExtension) {
|
||||
console.warn(`Aptabase: trackEvent requires a browser environment. Event "${opts.eventName}" will be discarded.`);
|
||||
return;
|
||||
}
|
||||
if (!opts.appKey) {
|
||||
console.warn(`Aptabase: init must be called before trackEvent. Event "${opts.eventName}" will be discarded.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(opts.apiUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"App-Key": opts.appKey
|
||||
},
|
||||
credentials: "omit",
|
||||
body: JSON.stringify({
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
sessionId: opts.sessionId,
|
||||
eventName: opts.eventName,
|
||||
systemProps: {
|
||||
locale: opts.locale ?? getBrowserLocale(),
|
||||
isDebug: opts.isDebug ?? getIsDebug(),
|
||||
appVersion: opts.appVersion ?? "",
|
||||
sdkVersion: opts.sdkVersion
|
||||
},
|
||||
props: opts.props
|
||||
})
|
||||
});
|
||||
if (response.status >= 300) {
|
||||
const responseBody = await response.text();
|
||||
console.warn(`Failed to send event "${opts.eventName}": ${response.status} ${responseBody}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to send event "${opts.eventName}"`);
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
function getBrowserLocale() {
|
||||
if (defaultLocale) {
|
||||
return defaultLocale;
|
||||
}
|
||||
if (typeof navigator === "undefined") {
|
||||
return void 0;
|
||||
}
|
||||
if (navigator.languages.length > 0) {
|
||||
defaultLocale = navigator.languages[0];
|
||||
} else {
|
||||
defaultLocale = navigator.language;
|
||||
}
|
||||
return defaultLocale;
|
||||
}
|
||||
function getIsDebug() {
|
||||
if (defaultIsDebug !== void 0) {
|
||||
return defaultIsDebug;
|
||||
}
|
||||
if (true) {
|
||||
defaultIsDebug = true;
|
||||
return defaultIsDebug;
|
||||
}
|
||||
if (typeof location === "undefined") {
|
||||
defaultIsDebug = false;
|
||||
return defaultIsDebug;
|
||||
}
|
||||
defaultIsDebug = location.hostname === "localhost";
|
||||
return defaultIsDebug;
|
||||
}
|
||||
|
||||
// src/index.ts
|
||||
var SESSION_TIMEOUT = 1 * 60 * 60;
|
||||
var sdkVersion = `aptabase-web@${"0.4.3"}`;
|
||||
var _appKey = "";
|
||||
var _apiUrl;
|
||||
var _options;
|
||||
function init(appKey, options) {
|
||||
if (!validateAppKey(appKey))
|
||||
return;
|
||||
_apiUrl = options?.apiUrl ?? getApiUrl(appKey, options);
|
||||
_appKey = appKey;
|
||||
_options = options;
|
||||
}
|
||||
async function trackEvent(eventName, props) {
|
||||
if (!_apiUrl)
|
||||
return;
|
||||
const sessionId = inMemorySessionId(SESSION_TIMEOUT);
|
||||
await sendEvent({
|
||||
apiUrl: _apiUrl,
|
||||
sessionId,
|
||||
appKey: _appKey,
|
||||
isDebug: _options?.isDebug,
|
||||
appVersion: _options?.appVersion,
|
||||
sdkVersion,
|
||||
eventName,
|
||||
props
|
||||
});
|
||||
}
|
||||
return __toCommonJS(src_exports);
|
||||
})();
|
||||
//# sourceMappingURL=aptabase.debug.js.map
|
||||
1
examples/simple-js/aptabase.min.js.map
Normal file
1
examples/simple-js/aptabase.min.js.map
Normal file
File diff suppressed because one or more lines are too long
38
examples/simple-js/example.html
Normal file
38
examples/simple-js/example.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Aptabase Standalone Example</title>
|
||||
<!-- <script src="https://static.hmac.kr/aptabase.min.js"></script> -->
|
||||
<!-- <script src="./packages/web/dist/aptabase.min.js"></script> -->
|
||||
<script src="./aptabase.min.js"></script>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 2em; }
|
||||
button { display: block; margin-bottom: 1em; padding: 0.5em 1em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Aptabase Standalone 테스트</h1>
|
||||
<p>Click the buttons below to send test events.</p>
|
||||
|
||||
<button onclick="track('app_started')">Track: App Started</button>
|
||||
<button onclick="track('feature_a_used', { plan: 'premium' })">Track: Used Feature A (with props)</button>
|
||||
<button onclick="track('feature_b_used')">Track: Used Feature B</button>
|
||||
<button onclick="track('user_signed_up')">Track: User Signed Up</button>
|
||||
<button onclick="track('item_purchased', { item_id: 'SKU-123', price: 9.99 })">Track: Item Purchased (with props)</button>
|
||||
|
||||
<script>
|
||||
// 1. Aptabase 초기화
|
||||
// App Key의 region이 'SH'인 경우 host는 필수입니다.
|
||||
window.aptabase.init("A-SH-7212023630", {
|
||||
host: "https://aptabase.hmac.kr"
|
||||
});
|
||||
|
||||
function track(eventName, props) {
|
||||
// 2. 이벤트 전송
|
||||
window.aptabase.trackEvent(eventName, props);
|
||||
console.log(`Event '${eventName}' sent!`, props ? { props } : '');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28099
package-lock.json
generated
Normal file
28099
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "root",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"examples/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build-packages": "turbo run build --filter=./packages/*",
|
||||
"ci:publish": "npm run build-packages && npx changeset publish",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.5.7",
|
||||
"prettier": "3.0.3",
|
||||
"tsup": "7.2.0",
|
||||
"turbo": "1.10.13",
|
||||
"@changesets/cli": "^2.27.8"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=7.0.0",
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120
|
||||
}
|
||||
}
|
||||
93
packages/angular/README.md
Normal file
93
packages/angular/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||

|
||||
|
||||
# Aptabase SDK for Angular Apps
|
||||
|
||||
A tiny SDK to instrument your Angular apps with Aptabase, an Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the SDK using npm or your preferred JavaScript package manager
|
||||
|
||||
```bash
|
||||
npm add @aptabase/angular
|
||||
```
|
||||
|
||||
2. Get your `App Key` from Aptabase, you can find it in the `Instructions` page.
|
||||
|
||||
3. Pass the `App Key` when initializing your app by importing a module or providing a function.
|
||||
|
||||
### Setup for Standalone API
|
||||
|
||||
Provide `provideAptabaseAnalytics` in the ApplicationConfig when bootstrapping.
|
||||
|
||||
```ts
|
||||
import { provideAptabaseAnalytics } from '@aptabase/angular';
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [..., provideAptabaseAnalytics('<YOUR_APP_KEY>')],
|
||||
}).catch((err) => console.error(err));
|
||||
```
|
||||
|
||||
[Full example here](examples/example-standalone/src/app)
|
||||
|
||||
### Setup for NgModules
|
||||
|
||||
Import `AptabaseAnalyticsModule` in your root AppModule.
|
||||
|
||||
```ts
|
||||
import { AptabaseAnalyticsModule } from '@aptabase/angular';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [..., AptabaseAnalyticsModule.forRoot('<YOUR_APP_KEY>')],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
```
|
||||
|
||||
[Full example here](examples/example-modules/src/app)
|
||||
|
||||
## Advanced setup
|
||||
|
||||
Both versions support also support an optional second parameter `AptabaseOptions` to pass in additional options.
|
||||
|
||||
```ts
|
||||
export type AptabaseOptions = {
|
||||
// Custom host for self-hosted Aptabase.
|
||||
host?: string;
|
||||
// Custom path for API endpoint. Useful when using reverse proxy.
|
||||
apiUrl?: string;
|
||||
// Defines the app version.
|
||||
appVersion?: string;
|
||||
// Defines whether the app is running on debug mode.
|
||||
isDebug?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## Tracking Events with Aptabase
|
||||
|
||||
After the initial setup the `AptabaseAnalyticsService` can be used to start tracking events.
|
||||
Simply inject the service in a component to start tracking events:
|
||||
|
||||
```ts
|
||||
import { AptabaseAnalyticsService } from '@aptabase/angular';
|
||||
|
||||
export class AppComponent {
|
||||
constructor(private _analyticsService: AptabaseAnalyticsService) {}
|
||||
|
||||
increment() {
|
||||
this.counter++;
|
||||
this._analyticsService.trackEvent('increment');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## A few important notes:
|
||||
|
||||
1. The SDK will automatically enhance the event with some useful information, like the OS and other properties.
|
||||
2. You're in control of what gets sent to Aptabase. This SDK does not automatically track any events, you need to call `trackEvent` manually.
|
||||
- Because of this, it's generally recommended to at least track an event at startup
|
||||
3. You do not need to subscribe to `trackEvent` function, it'll run in the background.
|
||||
4. Only strings and numeric values are allowed on custom properties.
|
||||
230
packages/angular/angular.json
Normal file
230
packages/angular/angular.json
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": ".",
|
||||
"projects": {
|
||||
"aptabase-angular": {
|
||||
"projectType": "library",
|
||||
"root": "aptabase-angular",
|
||||
"sourceRoot": "aptabase-angular/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "aptabase-angular/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "aptabase-angular/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "aptabase-angular/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "aptabase-angular/tsconfig.spec.json",
|
||||
"polyfills": ["zone.js", "zone.js/testing"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example-standalone": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"inlineTemplate": true,
|
||||
"inlineStyle": true,
|
||||
"style": "scss",
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:class": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"skipTests": true
|
||||
}
|
||||
},
|
||||
"root": "examples/example-standalone",
|
||||
"sourceRoot": "examples/example-standalone/src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/example-standalone",
|
||||
"index": "examples/example-standalone/src/index.html",
|
||||
"browser": "examples/example-standalone/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "examples/example-standalone/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["examples/example-standalone/src/favicon.ico", "examples/example-standalone/src/assets"],
|
||||
"styles": ["examples/example-standalone/src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "example-standalone:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "example-standalone:build:development"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"ssl": true
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "example-standalone:build"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example-modules": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"inlineTemplate": true,
|
||||
"inlineStyle": true,
|
||||
"style": "scss",
|
||||
"skipTests": true,
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:class": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"skipTests": true,
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"skipTests": true,
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"skipTests": true
|
||||
}
|
||||
},
|
||||
"root": "examples/example-modules",
|
||||
"sourceRoot": "examples/example-modules/src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/example-modules",
|
||||
"index": "examples/example-modules/src/index.html",
|
||||
"browser": "examples/example-modules/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "examples/example-modules/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["examples/example-modules/src/favicon.ico", "examples/example-modules/src/assets"],
|
||||
"styles": ["examples/example-modules/src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "example-modules:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "example-modules:build:development"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"ssl": true
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "example-modules:build"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
packages/angular/aptabase-angular/LICENSE
Normal file
21
packages/angular/aptabase-angular/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Sumbit Labs Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
93
packages/angular/aptabase-angular/README.md
Normal file
93
packages/angular/aptabase-angular/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||

|
||||
|
||||
# Aptabase SDK for Angular Apps
|
||||
|
||||
A tiny SDK to instrument your Angular apps with Aptabase, an Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the SDK using npm or your preferred JavaScript package manager
|
||||
|
||||
```bash
|
||||
npm add @aptabase/angular
|
||||
```
|
||||
|
||||
2. Get your `App Key` from Aptabase, you can find it in the `Instructions` page.
|
||||
|
||||
3. Pass the `App Key` when initializing your app by importing a module or providing a function.
|
||||
|
||||
### Setup for Standalone API
|
||||
|
||||
Provide `provideAptabaseAnalytics` in the ApplicationConfig when bootstrapping.
|
||||
|
||||
```ts
|
||||
import { provideAptabaseAnalytics } from '@aptabase/angular';
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [..., provideAptabaseAnalytics('<YOUR_APP_KEY>')],
|
||||
}).catch((err) => console.error(err));
|
||||
```
|
||||
|
||||
[Full example here](../examples/example-standalone/src/app)
|
||||
|
||||
### Setup for NgModules
|
||||
|
||||
Import `AptabaseAnalyticsModule` in your root AppModule.
|
||||
|
||||
```ts
|
||||
import { AptabaseAnalyticsModule } from '@aptabase/angular';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [..., AptabaseAnalyticsModule.forRoot('<YOUR_APP_KEY>')],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
```
|
||||
|
||||
[Full example here](../examples/example-modules/src/app)
|
||||
|
||||
## Advanced setup
|
||||
|
||||
Both versions support also support an optional second parameter `AptabaseOptions` to pass in additional options.
|
||||
|
||||
```ts
|
||||
export type AptabaseOptions = {
|
||||
// Custom host for self-hosted Aptabase.
|
||||
host?: string;
|
||||
// Custom path for API endpoint. Useful when using reverse proxy.
|
||||
apiUrl?: string;
|
||||
// Defines the app version.
|
||||
appVersion?: string;
|
||||
// Defines whether the app is running on debug mode.
|
||||
isDebug?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## Tracking Events with Aptabase
|
||||
|
||||
After the initial setup the `AptabaseAnalyticsService` can be used to start tracking events.
|
||||
Simply inject the service in a component to start tracking events:
|
||||
|
||||
```ts
|
||||
import { AptabaseAnalyticsService } from '@aptabase/angular';
|
||||
|
||||
export class AppComponent {
|
||||
constructor(private _analyticsService: AptabaseAnalyticsService) {}
|
||||
|
||||
increment() {
|
||||
this.counter++;
|
||||
this._analyticsService.trackEvent('increment');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## A few important notes:
|
||||
|
||||
1. The SDK will automatically enhance the event with some useful information, like the OS and other properties.
|
||||
2. You're in control of what gets sent to Aptabase. This SDK does not automatically track any events, you need to call `trackEvent` manually.
|
||||
- Because of this, it's generally recommended to at least track an event at startup
|
||||
3. You do not need to subscribe to `trackEvent` function, it'll run in the background.
|
||||
4. Only strings and numeric values are allowed on custom properties.
|
||||
7
packages/angular/aptabase-angular/ng-package.json
Normal file
7
packages/angular/aptabase-angular/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "dist",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
28
packages/angular/aptabase-angular/package.json
Normal file
28
packages/angular/aptabase-angular/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@aptabase/angular",
|
||||
"version": "0.0.6",
|
||||
"description": "Angular SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/aptabase/aptabase-js.git",
|
||||
"directory": "packages/angular/aptabase-angular"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/aptabase/aptabase-js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/aptabase/aptabase-js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"pre-build": "cp ../../shared.ts ./src",
|
||||
"build": "npm run pre-build && ng build aptabase-angular",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^17.0.0",
|
||||
"@angular/core": "^17.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { AptabaseOptions, getApiUrl, inMemorySessionId, sendEvent, validateAppKey } from '../shared';
|
||||
|
||||
// Session expires after 1 hour of inactivity
|
||||
// TODO move this to shared?
|
||||
const SESSION_TIMEOUT = 1 * 60 * 60;
|
||||
const pkgVersion = '0.0.0'; // bog: TODO fix this version
|
||||
const sdkVersion = `aptabase-angular@${pkgVersion}`;
|
||||
|
||||
export class AptabaseAnalyticsService {
|
||||
private _apiUrl: string | undefined;
|
||||
|
||||
constructor(
|
||||
private _appKey: string,
|
||||
private _options: AptabaseOptions,
|
||||
) {
|
||||
if (!validateAppKey(this._appKey)) return;
|
||||
|
||||
this._apiUrl = this._options.apiUrl ?? getApiUrl(this._appKey, this._options);
|
||||
}
|
||||
|
||||
async trackEvent(eventName: string, props?: Record<string, string | number | boolean>): Promise<void> {
|
||||
if (!this._apiUrl) return;
|
||||
|
||||
const sessionId = inMemorySessionId(SESSION_TIMEOUT);
|
||||
|
||||
await sendEvent({
|
||||
apiUrl: this._apiUrl,
|
||||
sessionId,
|
||||
appKey: this._appKey,
|
||||
isDebug: this._options?.isDebug,
|
||||
appVersion: this._options?.appVersion,
|
||||
sdkVersion,
|
||||
eventName,
|
||||
props,
|
||||
});
|
||||
}
|
||||
}
|
||||
28
packages/angular/aptabase-angular/src/lib/index.ts
Normal file
28
packages/angular/aptabase-angular/src/lib/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { EnvironmentProviders, NgModule, makeEnvironmentProviders } from '@angular/core';
|
||||
|
||||
import { AptabaseOptions } from '../shared';
|
||||
import { AptabaseAnalyticsService } from './analytics.service';
|
||||
|
||||
export function provideAptabaseAnalytics(appKey: string, options: AptabaseOptions = {}): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
{
|
||||
provide: AptabaseAnalyticsService,
|
||||
useValue: new AptabaseAnalyticsService(appKey, options),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@NgModule()
|
||||
export class AptabaseAnalyticsModule {
|
||||
static forRoot(appKey: string, options: AptabaseOptions = {}) {
|
||||
return {
|
||||
ngModule: AptabaseAnalyticsModule,
|
||||
providers: [
|
||||
{
|
||||
provide: AptabaseAnalyticsService,
|
||||
useValue: new AptabaseAnalyticsService(appKey, options),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
6
packages/angular/aptabase-angular/src/public-api.ts
Normal file
6
packages/angular/aptabase-angular/src/public-api.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Public API Surface of aptabase-angular
|
||||
*/
|
||||
|
||||
export * from './lib/analytics.service';
|
||||
export * from './lib/index';
|
||||
11
packages/angular/aptabase-angular/tsconfig.lib.json
Normal file
11
packages/angular/aptabase-angular/tsconfig.lib.json
Normal file
@@ -0,0 +1,11 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true
|
||||
},
|
||||
"exclude": ["**/*.spec.ts"]
|
||||
}
|
||||
10
packages/angular/aptabase-angular/tsconfig.lib.prod.json
Normal file
10
packages/angular/aptabase-angular/tsconfig.lib.prod.json
Normal file
@@ -0,0 +1,10 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
||||
14
packages/angular/aptabase-angular/tsconfig.spec.json
Normal file
14
packages/angular/aptabase-angular/tsconfig.spec.json
Normal file
@@ -0,0 +1,14 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
const routes: Routes = [];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { AptabaseAnalyticsService } from '@aptabase/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<h1>Welcome to {{ title }}!</h1>
|
||||
|
||||
<button (click)="trackButtonClick()">Track event</button>
|
||||
|
||||
<router-outlet />
|
||||
`,
|
||||
styles: [],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(private _analyticsService: AptabaseAnalyticsService) {}
|
||||
|
||||
title = 'example-modules';
|
||||
|
||||
trackButtonClick() {
|
||||
this._analyticsService.trackEvent('module_btn_click');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AptabaseAnalyticsModule } from '@aptabase/angular';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [BrowserModule, AppRoutingModule, AptabaseAnalyticsModule.forRoot('A-EU-1280395555')],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
BIN
packages/angular/examples/example-modules/src/favicon.ico
Normal file
BIN
packages/angular/examples/example-modules/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
packages/angular/examples/example-modules/src/index.html
Normal file
13
packages/angular/examples/example-modules/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ExampleModules</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
7
packages/angular/examples/example-modules/src/main.ts
Normal file
7
packages/angular/examples/example-modules/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@@ -0,0 +1,9 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/app"
|
||||
},
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { AptabaseAnalyticsService } from '@aptabase/angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
template: `
|
||||
<h1>Welcome to {{ title }}!</h1>
|
||||
|
||||
<button (click)="trackButtonClick()">Track event</button>
|
||||
|
||||
<router-outlet />
|
||||
`,
|
||||
styles: [],
|
||||
})
|
||||
export class AppComponent {
|
||||
private _analytics = inject(AptabaseAnalyticsService);
|
||||
|
||||
title = 'example-standalone';
|
||||
|
||||
trackButtonClick() {
|
||||
this._analytics.trackEvent('home_btn_click', { customProp: 'Hello from Aptabase' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideAptabaseAnalytics } from '@aptabase/angular';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes), provideAptabaseAnalytics('A-DEV-1624170742')],
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [];
|
||||
BIN
packages/angular/examples/example-standalone/src/favicon.ico
Normal file
BIN
packages/angular/examples/example-standalone/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
packages/angular/examples/example-standalone/src/index.html
Normal file
13
packages/angular/examples/example-standalone/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ExampleStandalone</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
5
packages/angular/examples/example-standalone/src/main.ts
Normal file
5
packages/angular/examples/example-standalone/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
|
||||
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@@ -0,0 +1,9 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/app"
|
||||
},
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
12758
packages/angular/package-lock.json
generated
Normal file
12758
packages/angular/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
packages/angular/package.json
Normal file
43
packages/angular/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@aptabase/angular",
|
||||
"private": true,
|
||||
"version": "0.0.2",
|
||||
"description": "Angular SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve example-standalone",
|
||||
"start-modules-sample": "ng serve example-modules",
|
||||
"pre-build": "cp ../shared.ts ./aptabase-angular/src",
|
||||
"build": "npm run pre-build && ng build aptabase-angular",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
"@angular/compiler": "^17.0.0",
|
||||
"@angular/core": "^17.0.0",
|
||||
"@angular/forms": "^17.0.0",
|
||||
"@angular/platform-browser": "^17.0.0",
|
||||
"@angular/platform-browser-dynamic": "^17.0.0",
|
||||
"@angular/router": "^17.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.7",
|
||||
"@angular/cli": "^17.0.8",
|
||||
"@angular/compiler-cli": "^17.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "20.5.7",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"ng-packagr": "^17.3.0",
|
||||
"typescript": "~5.2.2"
|
||||
}
|
||||
}
|
||||
36
packages/angular/tsconfig.json
Normal file
36
packages/angular/tsconfig.json
Normal file
@@ -0,0 +1,36 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"include": ["**/*.ts", "../shared/*.ts"],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@aptabase/angular": ["./aptabase-angular/src/public-api.ts"],
|
||||
"@aptabase/angular/*": ["./aptabase-angular/src/*"]
|
||||
},
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": ["ES2022", "dom"],
|
||||
"types": ["node", "chrome"]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
12
packages/browser/CHANGELOG.md
Normal file
12
packages/browser/CHANGELOG.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## 0.1.2
|
||||
|
||||
- Move browser locale and is debug reads from import to first event to better support SSR
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- Support for custom API path
|
||||
- use `installType` to determine if the extension is in debug mode
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial version of the Aptabase SDK for Browser Extensions
|
||||
21
packages/browser/LICENSE
Normal file
21
packages/browser/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Sumbit Labs Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
44
packages/browser/README.md
Normal file
44
packages/browser/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||

|
||||
|
||||
# Aptabase SDK for Browser Extensions
|
||||
|
||||
A tiny SDK (1 kB) to instrument your Browser/Chrome extensions with Aptabase, an Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps.
|
||||
|
||||
## Install
|
||||
|
||||
Install the SDK using npm or your preferred JavaScript package manager
|
||||
|
||||
```bash
|
||||
npm add @aptabase/browser
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to get your `App Key` from Aptabase, you can find it in the `Instructions` menu on the left side menu.
|
||||
|
||||
Initialize the SDK in your `Background Script` using your `App Key`:
|
||||
|
||||
```js
|
||||
import { init } from '@aptabase/browser';
|
||||
|
||||
init('<YOUR_APP_KEY>'); // 👈 this is where you enter your App Key
|
||||
```
|
||||
|
||||
The `init` function also supports an optional second parameter, which is an object with the `isDebug`property. Your data is segregated between development and production environments, so it's important to set this value correctly. By default the SDK will check if the extension was installed from an Extension Store, and if it wasn't it'll assume it's in development mode. If you want to override this behavior, you can set the`isDebug` property.
|
||||
|
||||
Afterwards you can start tracking events with `trackEvent` from anywhere in your extension:
|
||||
|
||||
```js
|
||||
import { trackEvent } from '@aptabase/browser';
|
||||
|
||||
trackEvent('connect_click'); // An event with no properties
|
||||
trackEvent('play_music', { name: 'Here comes the sun' }); // An event with a custom property
|
||||
```
|
||||
|
||||
A few important notes:
|
||||
|
||||
1. The SDK will automatically enhance the event with some useful information, like the OS, the app version, and other things.
|
||||
2. You're in control of what gets sent to Aptabase. This SDK does not automatically track any events, you need to call `trackEvent` manually.
|
||||
- Because of this, it's generally recommended to at least track an event at startup
|
||||
3. You do not need to await the `trackEvent` function, it'll run in the background.
|
||||
4. Only strings and numeric values are allowed on custom properties
|
||||
36
packages/browser/package.json
Normal file
36
packages/browser/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@aptabase/browser",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"description": "Browser Extension SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/aptabase/aptabase-js.git",
|
||||
"directory": "packages/browser"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/aptabase/aptabase-js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/aptabase/aptabase-js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"dist",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
4
packages/browser/src/global.d.ts
vendored
Normal file
4
packages/browser/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare var aptabase: {
|
||||
init: Function;
|
||||
trackEvent: Function;
|
||||
};
|
||||
91
packages/browser/src/index.ts
Normal file
91
packages/browser/src/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { getApiUrl, newSessionId, sendEvent, validateAppKey, type AptabaseOptions } from '../../shared';
|
||||
|
||||
// Session expires after 1 hour of inactivity
|
||||
const SESSION_TIMEOUT = 1 * 60 * 60;
|
||||
const sdkVersion = `aptabase-browser@${process.env.PKG_VERSION}`;
|
||||
const isWebContext = 'document' in globalThis;
|
||||
|
||||
let _appKey = '';
|
||||
let _apiUrl: string | undefined;
|
||||
let _options: AptabaseOptions | undefined;
|
||||
|
||||
globalThis.aptabase = {
|
||||
init,
|
||||
trackEvent,
|
||||
};
|
||||
|
||||
export async function init(appKey: string, options?: AptabaseOptions): Promise<void> {
|
||||
if (isWebContext) {
|
||||
console.error('@aptabase/browser can only be initialized in your background script.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateAppKey(appKey)) return;
|
||||
|
||||
const ext = await chrome.management.getSelf();
|
||||
|
||||
const isDebug = options?.isDebug ?? ext.installType === 'development';
|
||||
const appVersion = options?.appVersion ?? chrome.runtime.getManifest().version;
|
||||
|
||||
_apiUrl = options?.apiUrl ?? getApiUrl(appKey, options);
|
||||
_appKey = appKey;
|
||||
_options = { ...options, isDebug, appVersion };
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message && message.type === '__aptabase__trackEvent') {
|
||||
trackEvent(message.eventName, message.props);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function trackEvent(eventName: string, props?: Record<string, string | number | boolean>): Promise<void> {
|
||||
if (isWebContext) {
|
||||
return chrome.runtime.sendMessage({ type: '__aptabase__trackEvent', eventName, props });
|
||||
}
|
||||
|
||||
if (!_apiUrl) return;
|
||||
const sessionId = await getSessionId();
|
||||
|
||||
return sendEvent({
|
||||
apiUrl: _apiUrl,
|
||||
sessionId,
|
||||
appKey: _appKey,
|
||||
isDebug: _options?.isDebug,
|
||||
appVersion: _options?.appVersion,
|
||||
sdkVersion,
|
||||
eventName,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
type CachedItem = {
|
||||
id: string;
|
||||
lastTouched: number;
|
||||
};
|
||||
|
||||
let _sessionId = newSessionId();
|
||||
|
||||
async function getSessionId(): Promise<string> {
|
||||
const now = Date.now();
|
||||
|
||||
// If storage is not available, return the in memory session id
|
||||
// This id will be recreated if the extension is reloaded or the service worker becomes inactive
|
||||
if (!chrome?.storage?.local) return _sessionId;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get('_ab_session_id', (result) => {
|
||||
let item = (result['_ab_session_id'] as CachedItem) ?? { id: _sessionId, lastTouched: Date.now() };
|
||||
|
||||
const diffInMs = now - item.lastTouched;
|
||||
const diffInSec = Math.floor(diffInMs / 1000);
|
||||
if (diffInSec > SESSION_TIMEOUT) {
|
||||
item.id = newSessionId();
|
||||
}
|
||||
item.lastTouched = now;
|
||||
|
||||
chrome.storage.local.set({ _ab_session_id: item }, () => {
|
||||
resolve(item.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
15
packages/browser/tsconfig.json
Normal file
15
packages/browser/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"types": ["@types"]
|
||||
},
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist"
|
||||
}
|
||||
}
|
||||
15
packages/browser/tsup.config.ts
Normal file
15
packages/browser/tsup.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
const { version } = require('./package.json');
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
env: {
|
||||
PKG_VERSION: version,
|
||||
},
|
||||
});
|
||||
55
packages/react/CHANGELOG.md
Normal file
55
packages/react/CHANGELOG.md
Normal file
@@ -0,0 +1,55 @@
|
||||
## 0.3.5
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 0eb5c9f: react 19 as peer dep
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7ea8c11: fix: events tracked at page load are sometimes not sent
|
||||
|
||||
## 0.3.4
|
||||
|
||||
- Move browser locale and is debug reads from import to first event to better support SSR
|
||||
|
||||
## 0.3.3
|
||||
|
||||
- Rename `apiPath` to `apiUrl`
|
||||
|
||||
## 0.3.2
|
||||
|
||||
- Fix error when running on chrome
|
||||
|
||||
## 0.3.1
|
||||
|
||||
- Support for custom API path
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Internal refactor
|
||||
|
||||
## 0.2.2
|
||||
|
||||
- Updated dependencies
|
||||
|
||||
## 0.2.1
|
||||
|
||||
- Updated dependencies
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Updated dependencies
|
||||
|
||||
## 0.1.2
|
||||
|
||||
- Fixed an issue with client-side checking
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- Remove warning log from Server Side Rendering
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial version of the React SDK for Aptabase
|
||||
21
packages/react/LICENSE
Normal file
21
packages/react/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Sumbit Labs Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
154
packages/react/README.md
Normal file
154
packages/react/README.md
Normal file
@@ -0,0 +1,154 @@
|
||||

|
||||
|
||||
# Aptabase SDK for React Apps
|
||||
|
||||
A tiny SDK (1 kB) to instrument your React apps with Aptabase, an Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the SDK using npm or your preferred JavaScript package manager
|
||||
|
||||
```bash
|
||||
npm add @aptabase/react
|
||||
```
|
||||
|
||||
2. Get your `App Key` from Aptabase, you can find it in the `Instructions` menu on the left side menu.
|
||||
|
||||
3. Initialize the `AptabaseProvider` component to your app based on your framework.
|
||||
|
||||
<details>
|
||||
<summary>Setup for Next.js (App Router)</summary>
|
||||
|
||||
Add `AptabaseProvider` to your RootLayout component:
|
||||
|
||||
```tsx
|
||||
import { AptabaseProvider } from '@aptabase/react';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AptabaseProvider appKey="<YOUR_APP_KEY>">{children}</AptabaseProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Setup for Next.js (Pages Router)</summary>
|
||||
|
||||
Add `AptabaseProvider` to your `_app` component:
|
||||
|
||||
```tsx
|
||||
import { AptabaseProvider } from '@aptabase/react';
|
||||
import type { AppProps } from 'next/app';
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<AptabaseProvider appKey="<YOUR_APP_KEY>">
|
||||
<Component {...pageProps} />
|
||||
</AptabaseProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Setup for Remix</summary>
|
||||
|
||||
Add `AptabaseProvider` to your `entry.client.tsx` file:
|
||||
|
||||
```tsx
|
||||
import { AptabaseProvider } from '@aptabase/react';
|
||||
import { RemixBrowser } from '@remix-run/react';
|
||||
import { startTransition, StrictMode } from 'react';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<AptabaseProvider appKey="<YOUR_APP_KEY>">
|
||||
<RemixBrowser />
|
||||
</AptabaseProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Setup for Create React App or Vite</summary>
|
||||
|
||||
Add `AptabaseProvider` to your root component:
|
||||
|
||||
```tsx
|
||||
import { AptabaseProvider } from '@aptabase/react';
|
||||
|
||||
ReactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<AptabaseProvider appKey="<YOUR_APP_KEY>">
|
||||
<YourApp />
|
||||
</AptabaseProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Advanced setup
|
||||
|
||||
The `AptabaseProvider` also supports an optional second parameter, which is an object with the `appVersion` property.
|
||||
|
||||
It's up to you to decide how to get the version of your app, but it's generally recommended to use your bundler (like Webpack, Vite, Rollup, etc.) to inject the values at build time. Alternatively you can also pass it in manually.
|
||||
|
||||
It's up to you to decide what to get the version of your app, but it's generally recommended to use your bundler (like Webpack, Vite, Rollup, etc.) to inject the values at build time.
|
||||
|
||||
## Tracking Events with Aptabase
|
||||
|
||||
After setting up the `AptabaseProvider`, you can then start tracking events using the `useAptabase` hook.
|
||||
|
||||
Here's an example of a simple counter component that tracks the `increment` and `decrement` events:
|
||||
|
||||
```js
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useAptabase } from '@aptabase/react';
|
||||
|
||||
export function Counter() {
|
||||
const { trackEvent } = useAptabase();
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
function increment() {
|
||||
setCount((c) => c + 1);
|
||||
trackEvent('increment', { count });
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
setCount((c) => c - 1);
|
||||
trackEvent('decrement', { count });
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={increment}>Increment</button>
|
||||
<button onClick={decrement}>Decrement</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
A few important notes:
|
||||
|
||||
1. The SDK will automatically enhance the event with some useful information, like the OS and other properties.
|
||||
2. You're in control of what gets sent to Aptabase. This SDK does not automatically track any events, you need to call `trackEvent` manually.
|
||||
- Because of this, it's generally recommended to at least track an event at startup
|
||||
3. You do not need to await the `trackEvent` function, it'll run in the background.
|
||||
4. Only strings and numeric values are allowed on custom properties
|
||||
42
packages/react/package.json
Normal file
42
packages/react/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@aptabase/react",
|
||||
"version": "0.4.0",
|
||||
"type": "module",
|
||||
"description": "React SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/aptabase/aptabase-js.git",
|
||||
"directory": "packages/react"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/aptabase/aptabase-js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/aptabase/aptabase-js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"dist",
|
||||
"package.json"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
}
|
||||
104
packages/react/src/index.tsx
Normal file
104
packages/react/src/index.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { getApiUrl, inMemorySessionId, sendEvent, validateAppKey, type AptabaseOptions } from '../../shared';
|
||||
|
||||
// Session expires after 1 hour of inactivity
|
||||
const SESSION_TIMEOUT = 1 * 60 * 60;
|
||||
const sdkVersion = `aptabase-react@${process.env.PKG_VERSION}`;
|
||||
|
||||
let _appKey = '';
|
||||
let _apiUrl: string | undefined;
|
||||
let _options: AptabaseOptions | undefined;
|
||||
let _eventsBeforeInit: { [key: string]: Record<string, string | number | boolean> | undefined } = {};
|
||||
|
||||
type ContextProps = {
|
||||
appKey?: string;
|
||||
options?: AptabaseOptions;
|
||||
isInitialized: boolean;
|
||||
};
|
||||
|
||||
function init(appKey: string, options?: AptabaseOptions) {
|
||||
if (!validateAppKey(appKey)) return;
|
||||
|
||||
_apiUrl = options?.apiUrl ?? getApiUrl(appKey, options);
|
||||
_appKey = appKey;
|
||||
_options = options;
|
||||
|
||||
Object.keys(_eventsBeforeInit).forEach((eventName) => {
|
||||
trackEvent(eventName, _eventsBeforeInit[eventName]);
|
||||
});
|
||||
_eventsBeforeInit = {};
|
||||
}
|
||||
|
||||
async function trackEvent(eventName: string, props?: Record<string, string | number | boolean>): Promise<void> {
|
||||
if (!_apiUrl) return;
|
||||
|
||||
const sessionId = inMemorySessionId(SESSION_TIMEOUT);
|
||||
|
||||
await sendEvent({
|
||||
apiUrl: _apiUrl,
|
||||
sessionId,
|
||||
appKey: _appKey,
|
||||
isDebug: _options?.isDebug,
|
||||
appVersion: _options?.appVersion,
|
||||
sdkVersion,
|
||||
eventName,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
function trackEventBeforeInit(eventName: string, props?: Record<string, string | number | boolean>): Promise<void> {
|
||||
_eventsBeforeInit[eventName] = props;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const AptabaseContext = createContext<ContextProps>({ isInitialized: false });
|
||||
|
||||
type Props = {
|
||||
appKey: string;
|
||||
options?: AptabaseOptions;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export { type AptabaseOptions };
|
||||
|
||||
export type AptabaseClient = {
|
||||
trackEvent: typeof trackEvent;
|
||||
};
|
||||
|
||||
export function AptabaseProvider({ appKey, options, children }: Props) {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
init(appKey, options);
|
||||
setIsInitialized(true);
|
||||
}, [appKey, options]);
|
||||
|
||||
return <AptabaseContext.Provider value={{ appKey, options, isInitialized }}>{children}</AptabaseContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAptabase(): AptabaseClient {
|
||||
const ctx = useContext(AptabaseContext);
|
||||
|
||||
if (!ctx.appKey) {
|
||||
return {
|
||||
trackEvent: () => {
|
||||
console.warn(
|
||||
`Aptabase: useAptabase must be used within AptabaseProvider. Did you forget to wrap your app in <AptabaseProvider>?`,
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!ctx.isInitialized) {
|
||||
return {
|
||||
trackEvent: trackEventBeforeInit,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
trackEvent,
|
||||
};
|
||||
}
|
||||
15
packages/react/tsconfig.json
Normal file
15
packages/react/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"types": ["@types"]
|
||||
},
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist"
|
||||
}
|
||||
}
|
||||
15
packages/react/tsup.config.ts
Normal file
15
packages/react/tsup.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
const { version } = require('./package.json');
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.tsx'],
|
||||
format: ['cjs', 'esm'],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
env: {
|
||||
PKG_VERSION: version,
|
||||
},
|
||||
});
|
||||
161
packages/shared.ts
Normal file
161
packages/shared.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
let defaultLocale: string | undefined;
|
||||
let defaultIsDebug: boolean | undefined;
|
||||
const isInBrowser = typeof window !== 'undefined' && typeof window.fetch !== 'undefined';
|
||||
const isInBrowserExtension = typeof chrome !== 'undefined' && !!chrome.runtime?.id;
|
||||
|
||||
let _sessionId = newSessionId();
|
||||
let _lastTouched = new Date();
|
||||
|
||||
const _hosts: { [region: string]: string } = {
|
||||
US: 'https://us.aptabase.com',
|
||||
EU: 'https://eu.aptabase.com',
|
||||
DEV: 'https://localhost:3000',
|
||||
SH: '',
|
||||
};
|
||||
|
||||
export type AptabaseOptions = {
|
||||
// Custom host for self-hosted Aptabase.
|
||||
host?: string;
|
||||
// Custom path for API endpoint. Useful when using reverse proxy.
|
||||
apiUrl?: string;
|
||||
// Defines the app version.
|
||||
appVersion?: string;
|
||||
// Defines whether the app is running on debug mode.
|
||||
isDebug?: boolean;
|
||||
};
|
||||
|
||||
export function inMemorySessionId(timeout: number): string {
|
||||
let now = new Date();
|
||||
const diffInMs = now.getTime() - _lastTouched.getTime();
|
||||
const diffInSec = Math.floor(diffInMs / 1000);
|
||||
if (diffInSec > timeout) {
|
||||
_sessionId = newSessionId();
|
||||
}
|
||||
_lastTouched = now;
|
||||
|
||||
return _sessionId;
|
||||
}
|
||||
|
||||
export function newSessionId(): string {
|
||||
const epochInSeconds = Math.floor(Date.now() / 1000).toString();
|
||||
const random = Math.floor(Math.random() * 100000000)
|
||||
.toString()
|
||||
.padStart(8, '0');
|
||||
|
||||
return epochInSeconds + random;
|
||||
}
|
||||
|
||||
export function validateAppKey(appKey: string): boolean {
|
||||
const parts = appKey.split('-');
|
||||
if (parts.length !== 3 || _hosts[parts[1]] === undefined) {
|
||||
console.warn(`The Aptabase App Key "${appKey}" is invalid. Tracking will be disabled.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getApiUrl(appKey: string, options?: AptabaseOptions): string | undefined {
|
||||
const region = appKey.split('-')[1];
|
||||
if (region === 'SH') {
|
||||
if (!options?.host) {
|
||||
console.warn(`Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
return `${options.host}/api/v0/event`;
|
||||
}
|
||||
|
||||
const host = options?.host ?? _hosts[region];
|
||||
return `${host}/api/v0/event`;
|
||||
}
|
||||
|
||||
export async function sendEvent(opts: {
|
||||
apiUrl: string;
|
||||
appKey?: string;
|
||||
sessionId: string;
|
||||
locale?: string;
|
||||
isDebug?: boolean;
|
||||
appVersion?: string;
|
||||
sdkVersion: string;
|
||||
eventName: string;
|
||||
props?: Record<string, string | number | boolean>;
|
||||
}): Promise<void> {
|
||||
if (!isInBrowser && !isInBrowserExtension) {
|
||||
console.warn(`Aptabase: trackEvent requires a browser environment. Event "${opts.eventName}" will be discarded.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.appKey) {
|
||||
console.warn(`Aptabase: init must be called before trackEvent. Event "${opts.eventName}" will be discarded.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(opts.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'App-Key': opts.appKey,
|
||||
},
|
||||
credentials: 'omit',
|
||||
body: JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
sessionId: opts.sessionId,
|
||||
eventName: opts.eventName,
|
||||
systemProps: {
|
||||
locale: opts.locale ?? getBrowserLocale(),
|
||||
isDebug: opts.isDebug ?? getIsDebug(),
|
||||
appVersion: opts.appVersion ?? '',
|
||||
sdkVersion: opts.sdkVersion,
|
||||
},
|
||||
props: opts.props,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status >= 300) {
|
||||
const responseBody = await response.text();
|
||||
console.warn(`Failed to send event "${opts.eventName}": ${response.status} ${responseBody}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to send event "${opts.eventName}"`);
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
function getBrowserLocale(): string | undefined {
|
||||
if (defaultLocale) {
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
if (typeof navigator === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (navigator.languages.length > 0) {
|
||||
defaultLocale = navigator.languages[0];
|
||||
} else {
|
||||
defaultLocale = navigator.language;
|
||||
}
|
||||
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
function getIsDebug(): boolean {
|
||||
if (defaultIsDebug !== undefined) {
|
||||
return defaultIsDebug;
|
||||
}
|
||||
|
||||
if (process.env['NODE_ENV'] === 'development') {
|
||||
defaultIsDebug = true;
|
||||
return defaultIsDebug;
|
||||
}
|
||||
|
||||
if (typeof location === 'undefined') {
|
||||
defaultIsDebug = false;
|
||||
return defaultIsDebug;
|
||||
}
|
||||
|
||||
defaultIsDebug = location.hostname === 'localhost';
|
||||
|
||||
return defaultIsDebug;
|
||||
}
|
||||
48
packages/web/CHANGELOG.md
Normal file
48
packages/web/CHANGELOG.md
Normal file
@@ -0,0 +1,48 @@
|
||||
## 0.4.3
|
||||
|
||||
- Move browser locale and is debug reads from import to first event to better support SSR
|
||||
|
||||
## 0.4.2
|
||||
|
||||
- Fix error when running on chrome
|
||||
|
||||
## 0.4.1
|
||||
|
||||
- Support for custom API path
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- Internal refactor
|
||||
|
||||
## 0.3.2
|
||||
|
||||
- better version of the session id generator
|
||||
|
||||
## 0.3.1
|
||||
|
||||
- use new session id format
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Added an option to specify `isDebug` at init time
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Some internal refactor to support the new `@aptabase/react` package
|
||||
|
||||
## 0.1.3
|
||||
|
||||
- Add automatic session timeout after 1 hour of inactivity
|
||||
|
||||
## 0.1.2
|
||||
|
||||
- Added support for automatic segregation of Debug/Release events
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- Refactor on session generator
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Move to Rollup 3
|
||||
- Output both CJS and ESM modules
|
||||
21
packages/web/LICENSE
Normal file
21
packages/web/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Sumbit Labs Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
52
packages/web/README.md
Normal file
52
packages/web/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||

|
||||
|
||||
# Aptabase SDK for Web Apps
|
||||
|
||||
A tiny SDK (1 kB) to instrument your web app with Aptabase, an Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps.
|
||||
|
||||
Building a React app? Use the `@aptabase/react` package instead.
|
||||
|
||||
> 👉 **IMPORTANT**
|
||||
>
|
||||
> This SDK is for **Web Applications**, not websites. There's a subtle, but important difference. A web app is often a lot more interactive and does not cause a full page reload when the user interacts with it. It's often called a **Single-Page Application**. A website, on the other hand, is a lot more content-focused like marketing sites, landing pages, blogs, etc. While you can certainly use Aptabase to track events on websites, please be aware that each page reload will be considered a new session.
|
||||
|
||||
## Install
|
||||
|
||||
Install the SDK using npm or your preferred JavaScript package manager
|
||||
|
||||
```bash
|
||||
npm add @aptabase/web
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to get your `App Key` from Aptabase, you can find it in the `Instructions` menu on the left side menu.
|
||||
|
||||
Initialize the SDK using your `App Key`:
|
||||
|
||||
```js
|
||||
import { init } from '@aptabase/web';
|
||||
|
||||
init('<YOUR_APP_KEY>'); // 👈 this is where you enter your App Key
|
||||
```
|
||||
|
||||
The init function also supports an optional second parameter, which is an object with the `appVersion` property.
|
||||
|
||||
It's up to you to decide how to get the version of your app, but it's generally recommended to use your bundler (like Webpack, Vite, Rollup, etc.) to inject the values at build time. Alternatively you can also pass it in manually.
|
||||
|
||||
Afterwards you can start tracking events with `trackEvent`:
|
||||
|
||||
```js
|
||||
import { trackEvent } from '@aptabase/web';
|
||||
|
||||
trackEvent('connect_click'); // An event with no properties
|
||||
trackEvent('play_music', { name: 'Here comes the sun' }); // An event with a custom property
|
||||
```
|
||||
|
||||
A few important notes:
|
||||
|
||||
1. The SDK will automatically enhance the event with some useful information, like the OS and other properties.
|
||||
2. You're in control of what gets sent to Aptabase. This SDK does not automatically track any events, you need to call `trackEvent` manually.
|
||||
- Because of this, it's generally recommended to at least track an event at startup
|
||||
3. You do not need to await the `trackEvent` function, it'll run in the background.
|
||||
4. Only strings and numeric values are allowed on custom properties
|
||||
40
packages/web/package.json
Normal file
40
packages/web/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@aptabase/web",
|
||||
"version": "0.4.3",
|
||||
"type": "module",
|
||||
"description": "JavaScript SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/aptabase/aptabase-js.git",
|
||||
"directory": "packages/web"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/aptabase/aptabase-js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/aptabase/aptabase-js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"dist",
|
||||
"package.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chrome": "0.0.254",
|
||||
"@types/node": "20.5.7"
|
||||
}
|
||||
}
|
||||
36
packages/web/src/index.ts
Normal file
36
packages/web/src/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { getApiUrl, inMemorySessionId, sendEvent, validateAppKey, type AptabaseOptions } from '../../shared';
|
||||
|
||||
// Session expires after 1 hour of inactivity
|
||||
const SESSION_TIMEOUT = 1 * 60 * 60;
|
||||
const sdkVersion = `aptabase-web@${process.env.PKG_VERSION}`;
|
||||
|
||||
let _appKey = '';
|
||||
let _apiUrl: string | undefined;
|
||||
let _options: AptabaseOptions | undefined;
|
||||
|
||||
export { type AptabaseOptions };
|
||||
|
||||
export function init(appKey: string, options?: AptabaseOptions) {
|
||||
if (!validateAppKey(appKey)) return;
|
||||
|
||||
_apiUrl = options?.apiUrl ?? getApiUrl(appKey, options);
|
||||
_appKey = appKey;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
export async function trackEvent(eventName: string, props?: Record<string, string | number | boolean>): Promise<void> {
|
||||
if (!_apiUrl) return;
|
||||
|
||||
const sessionId = inMemorySessionId(SESSION_TIMEOUT);
|
||||
|
||||
await sendEvent({
|
||||
apiUrl: _apiUrl,
|
||||
sessionId,
|
||||
appKey: _appKey,
|
||||
isDebug: _options?.isDebug,
|
||||
appVersion: _options?.appVersion,
|
||||
sdkVersion,
|
||||
eventName,
|
||||
props,
|
||||
});
|
||||
}
|
||||
16
packages/web/tsconfig.json
Normal file
16
packages/web/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"types": ["@types"]
|
||||
},
|
||||
"types": ["chrome", "node"],
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist"
|
||||
}
|
||||
}
|
||||
55
packages/web/tsup.config.ts
Normal file
55
packages/web/tsup.config.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
const { version } = require('./package.json');
|
||||
|
||||
export default defineConfig([
|
||||
// Modern ESM and CJS bundles
|
||||
{
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
env: {
|
||||
PKG_VERSION: version,
|
||||
},
|
||||
},
|
||||
// IIFE bundle (for browser scripts)
|
||||
{
|
||||
entry: {
|
||||
'aptabase.debug': 'src/index.ts',
|
||||
},
|
||||
format: ['iife'],
|
||||
globalName: 'aptabase',
|
||||
outExtension() {
|
||||
return { js: `.js` }
|
||||
},
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: false, // has been cleaned in previous step
|
||||
define: {
|
||||
'process.env.PKG_VERSION': JSON.stringify(version),
|
||||
'process.env.NODE_ENV': JSON.stringify('development'),
|
||||
},
|
||||
},
|
||||
// Minified IIFE bundle
|
||||
{
|
||||
entry: {
|
||||
'aptabase.min': 'src/index.ts',
|
||||
},
|
||||
format: ['iife'],
|
||||
globalName: 'aptabase',
|
||||
minify: true,
|
||||
outExtension() {
|
||||
return { js: `.js` }
|
||||
},
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: false, // has been cleaned in previous step
|
||||
define: {
|
||||
'process.env.PKG_VERSION': JSON.stringify(version),
|
||||
'process.env.NODE_ENV': JSON.stringify('development'),
|
||||
},
|
||||
}
|
||||
])
|
||||
19828
pnpm-lock.yaml
generated
Normal file
19828
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user