diff --git a/README.md b/README.md index dab04438..3627884a 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,10 @@ Mixan is a simple analytics tool for logging events on web and react-native. My ### SDK - [*] Store duration on screen view events (can be done in backend as well) -- [ ] Create native sdk - - [ ] Handle sessions -- [ ] Create web sdk - - [ ] Screen view function should take in title, path and parse query string (especially utm tags) +- [x] Create native sdk + - [x] Handle sessions +- [x] Create web sdk + - [x] Screen view function should take in title, path and parse query string (especially utm tags) ## @mixan/sdk diff --git a/apps/test/README.md b/apps/test/README.md new file mode 100644 index 00000000..fba19eda --- /dev/null +++ b/apps/test/README.md @@ -0,0 +1,28 @@ +# Create T3 App + +This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. + +## What's next? How do I make an app with this? + +We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. + +If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. + +- [Next.js](https://nextjs.org) +- [NextAuth.js](https://next-auth.js.org) +- [Prisma](https://prisma.io) +- [Tailwind CSS](https://tailwindcss.com) +- [tRPC](https://trpc.io) + +## Learn More + +To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: + +- [Documentation](https://create.t3.gg/) +- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials + +You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! + +## How do I deploy this? + +Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. diff --git a/apps/test/next-env.d.ts b/apps/test/next-env.d.ts new file mode 100644 index 00000000..4f11a03d --- /dev/null +++ b/apps/test/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/test/next.config.mjs b/apps/test/next.config.mjs new file mode 100644 index 00000000..15d19f81 --- /dev/null +++ b/apps/test/next.config.mjs @@ -0,0 +1,23 @@ +/** + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful + * for Docker builds. + */ + +/** @type {import("next").NextConfig} */ +const config = { + reactStrictMode: false, + transpilePackages: ['@mixan/types', '@mixan/sdk', '@mixan/web-sdk'], + eslint: { ignoreDuringBuilds: true }, + typescript: { ignoreBuildErrors: true }, + /** + * If you are using `appDir` then you must comment the below `i18n` config out. + * + * @see https://github.com/vercel/next.js/issues/41980 + */ + i18n: { + locales: ['en'], + defaultLocale: 'en', + }, +}; + +export default config; diff --git a/apps/test/package.json b/apps/test/package.json new file mode 100644 index 00000000..61a37fd1 --- /dev/null +++ b/apps/test/package.json @@ -0,0 +1,49 @@ +{ + "name": "@mixan/test", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint .", + "format": "prettier --write \"**/*.{tsx,mjs,ts,md,json}\"", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "next": "13.4", + "react": "18.2.0", + "react-dom": "18.2.0", + "@mixan-test/sdk": "workspace:@mixan/sdk@*", + "@mixan-test/sdk-web": "workspace:@mixan/sdk-web@*" + }, + "devDependencies": { + "@mixan/eslint-config": "workspace:*", + "@mixan/prettier-config": "workspace:*", + "@mixan/tsconfig": "workspace:*", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@types/react-syntax-highlighter": "^15.5.9", + "@typescript-eslint/eslint-plugin": "^6.6.0", + "@typescript-eslint/parser": "^6.6.0", + "autoprefixer": "^10.4.14", + "eslint": "^8.48.0", + "postcss": "^8.4.27", + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.1", + "tailwindcss": "^3.3.3", + "typescript": "^5.2.2" + }, + "ct3aMetadata": { + "initVersion": "7.21.0" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@mixan/eslint-config/base", + "@mixan/eslint-config/nextjs", + "@mixan/eslint-config/react" + ] + }, + "prettier": "@mixan/prettier-config" +} diff --git a/apps/test/postcss.config.cjs b/apps/test/postcss.config.cjs new file mode 100644 index 00000000..e305dd92 --- /dev/null +++ b/apps/test/postcss.config.cjs @@ -0,0 +1,8 @@ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +module.exports = config; diff --git a/apps/test/public/favicon.ico b/apps/test/public/favicon.ico new file mode 100644 index 00000000..b5336a48 Binary files /dev/null and b/apps/test/public/favicon.ico differ diff --git a/apps/test/src/analytics.ts b/apps/test/src/analytics.ts new file mode 100644 index 00000000..32ec747f --- /dev/null +++ b/apps/test/src/analytics.ts @@ -0,0 +1,11 @@ +import { MixanWeb } from '@mixan-test/sdk-web'; + +export const mixan = new MixanWeb({ + verbose: true, + url: process.env.NEXT_PUBLIC_MIXAN_URL!, + clientId: process.env.NEXT_PUBLIC_MIXAN_CLIENT_ID!, + clientSecret: process.env.NEXT_PUBLIC_MIXAN_CLIENT_SECRET!, + trackIp: true, +}); + +mixan.trackOutgoingLinks(); diff --git a/apps/test/src/pages/_app.tsx b/apps/test/src/pages/_app.tsx new file mode 100644 index 00000000..3a6c316e --- /dev/null +++ b/apps/test/src/pages/_app.tsx @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; +import { mixan } from '@/analytics'; +import type { AppProps } from 'next/app'; +import { useRouter } from 'next/router'; + +export default function MyApp({ Component, pageProps }: AppProps) { + const router = useRouter(); + useEffect(() => { + mixan.init(); + return router.events.on('routeChangeComplete', () => { + mixan.screenView(); + }); + }, []); + return ; +} diff --git a/apps/test/src/pages/index.tsx b/apps/test/src/pages/index.tsx new file mode 100644 index 00000000..396a6ece --- /dev/null +++ b/apps/test/src/pages/index.tsx @@ -0,0 +1,14 @@ +import Link from 'next/link'; + +export default function Home() { + return ( +
+ Test + Google + KiddoKitchen + + KiddoKitchen (_blank) + +
+ ); +} diff --git a/apps/test/src/pages/test.tsx b/apps/test/src/pages/test.tsx new file mode 100644 index 00000000..8d670b4e --- /dev/null +++ b/apps/test/src/pages/test.tsx @@ -0,0 +1,9 @@ +import Link from 'next/link'; + +export default function Test() { + return ( +
+ Home +
+ ); +} diff --git a/apps/test/tailwind.config.js b/apps/test/tailwind.config.js new file mode 100644 index 00000000..931a5665 --- /dev/null +++ b/apps/test/tailwind.config.js @@ -0,0 +1,102 @@ +const colors = [ + '#7856ff', + '#ff7557', + '#7fe1d8', + '#f8bc3c', + '#b3596e', + '#72bef4', + '#ffb27a', + '#0f7ea0', + '#3ba974', + '#febbb2', + '#cb80dc', + '#5cb7af', +]; + +/** @type {import('tailwindcss').Config} */ +const config = { + safelist: [...colors.map((color) => `chart-${color}`)], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + ...colors.reduce((acc, color, index) => { + return { + ...acc, + [`chart-${index}`]: color, + }; + }, {}), + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + boxShadow: { + DEFAULT: '0 5px 10px rgb(0 0 0 / 5%)', + }, + keyframes: { + 'accordion-down': { + from: { height: '0px' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0px' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; + +export default config; diff --git a/apps/test/tsconfig.json b/apps/test/tsconfig.json new file mode 100644 index 00000000..b62436fc --- /dev/null +++ b/apps/test/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@mixan/tsconfig/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "plugins": [ + { + "name": "next" + } + ], + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": [".", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/sdk-native/index.ts b/packages/sdk-native/index.ts new file mode 100644 index 00000000..e322aab1 --- /dev/null +++ b/packages/sdk-native/index.ts @@ -0,0 +1,28 @@ +import type { NewMixanOptions } from '@mixan/sdk'; +import { Mixan } from '@mixan/sdk'; + +export class MixanNative extends Mixan { + constructor(options: NewMixanOptions) { + super(options); + } + + async properties() { + return { + ip: await super.ip(), + }; + } + + async init(properties: Record) { + super.init({ + ...(await this.properties()), + ...(properties ?? {}), + }); + } + + screenView(route: string, properties?: Record): void { + super.event('screen_view', { + ...properties, + route: route, + }); + } +} diff --git a/packages/sdk-native/package.json b/packages/sdk-native/package.json new file mode 100644 index 00000000..aa3da126 --- /dev/null +++ b/packages/sdk-native/package.json @@ -0,0 +1,31 @@ +{ + "name": "@mixan/sdk-native", + "version": "0.0.1", + "module": "index.ts", + "scripts": { + "build": "rm -rf dist && tsup", + "lint": "eslint .", + "format": "prettier --check \"**/*.{mjs,ts,md,json}\"", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@mixan/sdk": "workspace:*", + "@mixan/types": "workspace:*" + }, + "devDependencies": { + "@mixan/eslint-config": "workspace:*", + "@mixan/prettier-config": "workspace:*", + "@mixan/tsconfig": "workspace:*", + "eslint": "^8.48.0", + "prettier": "^3.0.3", + "tsup": "^7.2.0", + "typescript": "^5.2.2" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@mixan/eslint-config/base" + ] + }, + "prettier": "@mixan/prettier-config" +} diff --git a/packages/sdk-native/src/utils.ts b/packages/sdk-native/src/utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/sdk-native/tsconfig.json b/packages/sdk-native/tsconfig.json new file mode 100644 index 00000000..257bf27a --- /dev/null +++ b/packages/sdk-native/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@mixan/tsconfig/sdk.json", + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/packages/sdk-native/tsup.config.ts b/packages/sdk-native/tsup.config.ts new file mode 100644 index 00000000..5fea5195 --- /dev/null +++ b/packages/sdk-native/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; + +import config from '@mixan/tsconfig/tsup.config.json' assert { + type: 'json' +} + +export default defineConfig(config as any); diff --git a/packages/sdk-web/index.ts b/packages/sdk-web/index.ts new file mode 100644 index 00000000..6fb9fd42 --- /dev/null +++ b/packages/sdk-web/index.ts @@ -0,0 +1,97 @@ +import type { NewMixanOptions } from '@mixan/sdk'; +import { Mixan } from '@mixan/sdk'; + +import { parseQuery } from './src/parseQuery'; +import { getDevice, getOS, getTimezone } from './src/utils'; + +type Omit = Pick>; +type PartialBy = Omit & Partial>; + +export class MixanWeb extends Mixan { + constructor( + options: PartialBy + ) { + super({ + batchInterval: options.batchInterval ?? 1000, + setItem: + typeof localStorage === 'undefined' + ? () => {} + : localStorage.setItem.bind(localStorage), + removeItem: + typeof localStorage === 'undefined' + ? () => {} + : localStorage.removeItem.bind(localStorage), + getItem: + typeof localStorage === 'undefined' + ? () => null + : localStorage.getItem.bind(localStorage), + ...options, + }); + } + + isServer() { + return typeof document === 'undefined'; + } + + async properties() { + return { + ip: await super.ip(), + os: getOS(), + device: getDevice(), + ua: navigator.userAgent, + referrer: document.referrer, + language: navigator.language, + timezone: getTimezone(), + screen: { + width: window.screen.width, + height: window.screen.height, + pixelRatio: window.devicePixelRatio, + }, + }; + } + + async init(properties?: Record) { + if (this.isServer()) { + return; + } + + super.init({ + ...(await this.properties()), + ...(properties ?? {}), + }); + this.screenView(); + } + + trackOutgoingLinks() { + if (this.isServer()) { + return; + } + + document.addEventListener('click', (event) => { + const target = event.target as HTMLElement; + if (target.tagName === 'A') { + const href = target.getAttribute('href'); + if (href?.startsWith('http')) { + super.event('link_out', { + href, + text: target.innerText, + }); + super.flush(); + } + } + }); + } + + screenView(properties?: Record): void { + if (this.isServer()) { + return; + } + + super.event('screen_view', { + ...properties, + route: window.location.pathname, + url: window.location.href, + query: parseQuery(window.location.search ?? ''), + }); + } +} diff --git a/packages/sdk-web/package.json b/packages/sdk-web/package.json new file mode 100644 index 00000000..da353350 --- /dev/null +++ b/packages/sdk-web/package.json @@ -0,0 +1,31 @@ +{ + "name": "@mixan/sdk-web", + "version": "0.0.1", + "module": "index.ts", + "scripts": { + "build": "rm -rf dist && tsup", + "lint": "eslint .", + "format": "prettier --check \"**/*.{mjs,ts,md,json}\"", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@mixan/sdk": "workspace:*", + "@mixan/types": "workspace:*" + }, + "devDependencies": { + "@mixan/eslint-config": "workspace:*", + "@mixan/prettier-config": "workspace:*", + "@mixan/tsconfig": "workspace:*", + "eslint": "^8.48.0", + "prettier": "^3.0.3", + "tsup": "^7.2.0", + "typescript": "^5.2.2" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@mixan/eslint-config/base" + ] + }, + "prettier": "@mixan/prettier-config" +} diff --git a/packages/sdk-web/src/parseQuery.ts b/packages/sdk-web/src/parseQuery.ts new file mode 100644 index 00000000..a7f7f346 --- /dev/null +++ b/packages/sdk-web/src/parseQuery.ts @@ -0,0 +1,8 @@ +export function parseQuery(query: string): Record { + const params = new URLSearchParams(query); + const result: Record = {}; + for (const [key, value] of params.entries()) { + result[key] = value; + } + return result; +} diff --git a/packages/sdk-web/src/utils.ts b/packages/sdk-web/src/utils.ts new file mode 100644 index 00000000..ca1cefc4 --- /dev/null +++ b/packages/sdk-web/src/utils.ts @@ -0,0 +1,66 @@ +export function getOS() { + if (/iPad/i.test(navigator.userAgent)) { + return 'iPad'; + } + if (/iPhone/i.test(navigator.userAgent)) { + return 'iPhone'; + } + if (/iPod/i.test(navigator.userAgent)) { + return 'iPod'; + } + if (/Macintosh/i.test(navigator.userAgent)) { + return 'macOS'; + } + if (/IEMobile|Windows/i.test(navigator.userAgent)) { + return 'Windows'; + } + if (/Android/i.test(navigator.userAgent)) { + return 'Android'; + } + if (/BlackBerry/i.test(navigator.userAgent)) { + return 'BlackBerry'; + } + if (/EF500/i.test(navigator.userAgent)) { + return 'Bluebird'; + } + if (/CrOS/i.test(navigator.userAgent)) { + return 'Chrome OS'; + } + if (/DL-AXIS/i.test(navigator.userAgent)) { + return 'Datalogic'; + } + if (/CT50/i.test(navigator.userAgent)) { + return 'Honeywell'; + } + if (/TC70|TC55/i.test(navigator.userAgent)) { + return 'Zebra'; + } + if (/Linux/i.test(navigator.userAgent)) { + return 'Generic Linux'; + } + return 'Unknown'; +} + +export function getDevice() { + const ua = navigator.userAgent; + const t1 = + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + ua + ); + const t2 = + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + ua.slice(0, 4) + ); + if (t1 || t2) { + return 'mobile'; + } + return 'desktop'; +} + +export function getTimezone() { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch (e) { + return 'unknown'; + } +} diff --git a/packages/sdk-web/tsconfig.json b/packages/sdk-web/tsconfig.json new file mode 100644 index 00000000..257bf27a --- /dev/null +++ b/packages/sdk-web/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@mixan/tsconfig/sdk.json", + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/packages/sdk-web/tsup.config.ts b/packages/sdk-web/tsup.config.ts new file mode 100644 index 00000000..5fea5195 --- /dev/null +++ b/packages/sdk-web/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; + +import config from '@mixan/tsconfig/tsup.config.json' assert { + type: 'json' +} + +export default defineConfig(config as any); diff --git a/packages/sdk/index.ts b/packages/sdk/index.ts index f18680c5..df835f84 100644 --- a/packages/sdk/index.ts +++ b/packages/sdk/index.ts @@ -11,10 +11,12 @@ export interface NewMixanOptions { batchInterval?: number; maxBatchSize?: number; sessionTimeout?: number; + session?: boolean; verbose?: boolean; - saveProfileId: (profiId: string) => void; - getProfileId: () => string | null; - removeProfileId: () => void; + setItem: (key: string, profileId: string) => void; + getItem: (key: string) => string | null; + removeItem: (key: string) => void; + trackIp?: boolean; } export type MixanOptions = Required; @@ -39,7 +41,6 @@ class Fetcher { const url = `${this.url}${path}`; this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2)); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call return fetch(url, { headers: { ['mixan-client-id']: this.clientId, @@ -137,19 +138,23 @@ export class Mixan { private options: MixanOptions; private logger: (...args: any[]) => void; private globalProperties: Record = {}; - private lastEventAt?: string; - private lastScreenViewAt?: string; + private lastEventAt: string; + private promiseIp: Promise; constructor(options: NewMixanOptions) { this.logger = options.verbose ? console.log : () => {}; this.options = { sessionTimeout: 1000 * 60 * 30, + session: true, verbose: false, batchInterval: 10000, maxBatchSize: 10, + trackIp: false, ...options, }; + this.lastEventAt = + this.options.getItem('@mixan:lastEventAt') ?? '1970-01-01'; this.fetch = new Fetcher(this.options); this.eventBatcher = new Batcher(this.options, (queue) => { this.fetch.post( @@ -164,13 +169,26 @@ export class Mixan { })) ); }); + + this.promiseIp = this.options.trackIp + ? fetch('https://api.ipify.org') + .then((res) => res.text()) + .catch(() => null) + : Promise.resolve(null); } - timestamp() { - return new Date().toISOString(); + async ip() { + return this.promiseIp; } - init() { + timestamp(modify = 0) { + return new Date(Date.now() + modify).toISOString(); + } + + init(properties?: Record) { + if (properties) { + this.setGlobalProperties(properties); + } this.logger('Mixan: Init'); this.setAnonymousUser(); } @@ -178,14 +196,15 @@ export class Mixan { event(name: string, properties: Record = {}) { const now = new Date(); const isSessionStart = - now.getTime() - new Date(this.lastEventAt ?? '1970-01-01').getTime() > - this.options.sessionTimeout; + this.options.session && + now.getTime() - new Date(this.lastEventAt).getTime() > + this.options.sessionTimeout; if (isSessionStart) { this.logger('Mixan: Session start'); this.eventBatcher.add({ name: 'session_start', - time: this.timestamp(), + time: this.timestamp(-10), properties: {}, profileId: this.profileId ?? null, }); @@ -199,19 +218,28 @@ export class Mixan { profileId: this.profileId ?? null, }); this.lastEventAt = this.timestamp(); + this.options.setItem('@mixan:lastEventAt', this.lastEventAt); } private async setAnonymousUser(retryCount = 0) { - const profileId = this.options.getProfileId(); + const profileId = this.options.getItem('@mixan:profileId'); if (profileId) { this.profileId = profileId; + await this.setUser({ + properties: this.globalProperties, + }); this.logger('Mixan: Use existing profile', this.profileId); } else { - const res = await this.fetch.post('/profiles'); + const res = await this.fetch.post( + '/profiles', + { + properties: this.globalProperties, + } + ); if (res) { this.profileId = res.id; - this.options.saveProfileId(res.id); + this.options.setItem('@mixan:profileId', res.id); this.logger('Mixan: Create new profile', this.profileId); } else if (retryCount < 2) { setTimeout(() => { @@ -249,8 +277,16 @@ export class Mixan { } setGlobalProperties(properties: Record) { + if (typeof properties !== 'object') { + return this.logger( + 'Mixan: Set global properties failed, properties must be an object' + ); + } this.logger('Mixan: Set global properties', properties); - this.globalProperties = properties ?? {}; + this.globalProperties = { + ...this.globalProperties, + ...properties, + }; } async increment(name: string, value = 1) { @@ -291,34 +327,16 @@ export class Mixan { ); } - screenView(route: string, _properties?: Record) { - const properties = _properties ?? {}; - const now = new Date(); - - if (this.lastScreenViewAt) { - const last = new Date(this.lastScreenViewAt); - const diff = now.getTime() - last.getTime(); - this.logger(`Mixan: Screen view duration: ${diff}ms`); - properties.duration = diff; - } - - this.lastScreenViewAt = now.toISOString(); - this.event('screen_view', { - ...properties, - route, - }); - } - flush() { this.logger('Mixan: Flushing events queue'); this.eventBatcher.send(); - this.lastScreenViewAt = undefined; } clear() { this.logger('Mixan: Clear, send remaining events and remove profileId'); this.eventBatcher.send(); - this.options.removeProfileId(); + this.options.removeItem('@mixan:profileId'); + this.options.removeItem('@mixan:session'); this.profileId = undefined; this.setAnonymousUser(); } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 7baf1e7d..a289886e 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "module": "index.ts", "scripts": { + "build": "rm -rf dist && tsup", "lint": "eslint .", "format": "prettier --check \"**/*.{mjs,ts,md,json}\"", "typecheck": "tsc --noEmit" @@ -13,6 +14,7 @@ "devDependencies": { "@mixan/eslint-config": "workspace:*", "@mixan/prettier-config": "workspace:*", + "@mixan/tsconfig": "workspace:*", "eslint": "^8.48.0", "prettier": "^3.0.3", "tsup": "^7.2.0", diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index d539d24d..257bf27a 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,21 +1,6 @@ { + "extends": "@mixan/tsconfig/sdk.json", "compilerOptions": { - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "module": "esnext", - "target": "esnext", - "moduleResolution": "bundler", - "moduleDetection": "force", - "composite": true, - "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "react-jsx", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - - "outDir": "dist", - "allowImportingTsExtensions": false, - "noEmit": false + "outDir": "dist" } } diff --git a/packages/sdk/tsup.config.ts b/packages/sdk/tsup.config.ts index ef86be71..5fea5195 100644 --- a/packages/sdk/tsup.config.ts +++ b/packages/sdk/tsup.config.ts @@ -1,10 +1,7 @@ import { defineConfig } from 'tsup'; -export default defineConfig({ - entry: ['index.ts'], - format: ['cjs', 'esm'], // Build for commonJS and ESmodules - dts: true, // Generate declaration file (.d.ts) - splitting: false, - sourcemap: true, - clean: true, -}); +import config from '@mixan/tsconfig/tsup.config.json' assert { + type: 'json' +} + +export default defineConfig(config as any); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9787164..a393b6ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,70 @@ importers: specifier: ^7.5.4 version: 7.5.4 + apps/test: + dependencies: + '@mixan-test/sdk': + specifier: workspace:@mixan/sdk@* + version: link:../../packages/sdk + '@mixan-test/sdk-web': + specifier: workspace:@mixan/sdk-web@* + version: link:../../packages/sdk-web + next: + specifier: '13.4' + version: 13.4.19(react-dom@18.2.0)(react@18.2.0) + react: + specifier: 18.2.0 + version: 18.2.0 + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@mixan/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@mixan/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@mixan/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + '@types/react': + specifier: ^18.2.20 + version: 18.2.34 + '@types/react-dom': + specifier: ^18.2.7 + version: 18.2.14 + '@types/react-syntax-highlighter': + specifier: ^15.5.9 + version: 15.5.9 + '@typescript-eslint/eslint-plugin': + specifier: ^6.6.0 + version: 6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': + specifier: ^6.6.0 + version: 6.9.1(eslint@8.52.0)(typescript@5.2.2) + autoprefixer: + specifier: ^10.4.14 + version: 10.4.16(postcss@8.4.31) + eslint: + specifier: ^8.48.0 + version: 8.52.0 + postcss: + specifier: ^8.4.27 + version: 8.4.31 + prettier: + specifier: ^3.0.3 + version: 3.0.3 + prettier-plugin-tailwindcss: + specifier: ^0.5.1 + version: 0.5.6(prettier@3.0.3) + tailwindcss: + specifier: ^3.3.3 + version: 3.3.5 + typescript: + specifier: ^5.2.2 + version: 5.2.2 + apps/web: dependencies: '@hookform/resolvers': @@ -242,6 +306,71 @@ importers: '@mixan/prettier-config': specifier: workspace:* version: link:../../tooling/prettier + '@mixan/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + eslint: + specifier: ^8.48.0 + version: 8.52.0 + prettier: + specifier: ^3.0.3 + version: 3.0.3 + tsup: + specifier: ^7.2.0 + version: 7.2.0(typescript@5.2.2) + typescript: + specifier: ^5.2.2 + version: 5.2.2 + + packages/sdk-native: + dependencies: + '@mixan/sdk': + specifier: workspace:* + version: link:../sdk + '@mixan/types': + specifier: workspace:* + version: link:../types + devDependencies: + '@mixan/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@mixan/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@mixan/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + eslint: + specifier: ^8.48.0 + version: 8.52.0 + prettier: + specifier: ^3.0.3 + version: 3.0.3 + tsup: + specifier: ^7.2.0 + version: 7.2.0(typescript@5.2.2) + typescript: + specifier: ^5.2.2 + version: 5.2.2 + + packages/sdk-web: + dependencies: + '@mixan/sdk': + specifier: workspace:* + version: link:../sdk + '@mixan/types': + specifier: workspace:* + version: link:../types + devDependencies: + '@mixan/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@mixan/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@mixan/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript eslint: specifier: ^8.48.0 version: 8.52.0 @@ -362,6 +491,12 @@ importers: '@types/eslint': specifier: ^8.44.2 version: 8.44.6 + '@types/node': + specifier: ^18.16.0 + version: 18.18.8 + '@types/semver': + specifier: ^7.5.4 + version: 7.5.4 eslint: specifier: ^8.48.0 version: 8.52.0 @@ -372,7 +507,11 @@ importers: specifier: ^5.2.2 version: 5.2.2 - tooling/typescript: {} + tooling/typescript: + devDependencies: + tsup: + specifier: ^7.2.0 + version: 7.2.0(typescript@5.2.2) packages: diff --git a/tooling/publish/package.json b/tooling/publish/package.json index f00d868b..27ec99f3 100644 --- a/tooling/publish/package.json +++ b/tooling/publish/package.json @@ -17,6 +17,8 @@ "@mixan/prettier-config": "workspace:*", "@mixan/tsconfig": "workspace:*", "@types/eslint": "^8.44.2", + "@types/node": "^18.16.0", + "@types/semver": "^7.5.4", "eslint": "^8.48.0", "prettier": "^3.0.3", "typescript": "^5.2.2" diff --git a/tooling/typescript/package.json b/tooling/typescript/package.json index 21d3d700..a881fe78 100644 --- a/tooling/typescript/package.json +++ b/tooling/typescript/package.json @@ -3,6 +3,11 @@ "version": "0.1.0", "private": true, "files": [ - "base.json" - ] + "base.json", + "sdk.json", + "tsup.config.json" + ], + "devDependencies": { + "tsup": "^7.2.0" + } } diff --git a/tooling/typescript/sdk.json b/tooling/typescript/sdk.json new file mode 100644 index 00000000..4992b259 --- /dev/null +++ b/tooling/typescript/sdk.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "composite": false, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "allowImportingTsExtensions": false, + "noEmit": false + } +} diff --git a/tooling/typescript/tsup.config.json b/tooling/typescript/tsup.config.json new file mode 100644 index 00000000..96fbf9a3 --- /dev/null +++ b/tooling/typescript/tsup.config.json @@ -0,0 +1,8 @@ +{ + "entry": ["index.ts"], + "format": ["cjs", "esm"], + "dts": true, + "splitting": false, + "sourcemap": true, + "clean": true +}