refacto/migrate-turborepo

This commit is contained in:
Simon Boisset 2023-08-21 23:43:50 +02:00
parent 9905366e9a
commit a9507e7d3d
134 changed files with 20842 additions and 475 deletions

5
packages/electron/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
dist
example/node_modules
example/dist-electron
example/dist

10
packages/electron/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
"editor.formatOnSave": true,
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"typescript.tsserver.experimental.enableProjectDiagnostics": true
}

View file

@ -0,0 +1,31 @@
## 0.2.2
Reduced minimum Electron version to v8.x
## 0.2.1
Fix typescript definitions
## 0.2.0
Preload script is no longer required, the SDK now uses fetch via a custom protocol
## 0.1.4
Refactored submodules to simplify setup
## 0.1.3
Added notice when importing `@aptabase/electron` module
## 0.1.2
Fix for disabled contextIsolation
## 0.1.1
Support for contextIsolation
## 0.1.0
Initial release of Electron SDK

21
packages/electron/LICENSE Normal file
View 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.

View file

@ -0,0 +1,51 @@
![Aptabase](https://aptabase.com/og.png)
# Electron SDK for Aptabase
Instrument your Electron apps with Aptabase, an Open Source, Privacy-First, and Simple Analytics for Mobile, Desktop, and Web Apps.
## Install
Install the SDK using your preferred JavaScript package manager
```bash
npm add @aptabase/electron
```
## Usage
First, you need to get your `App Key` from Aptabase, you can find it in the `Instructions` menu on the left side menu.
On your Electron main's process, initialize the SDK before the app is ready:
```js
import { initialize } from "@aptabase/electron/main";
initialize("<YOUR_APP_KEY>"); // 👈 this is where you enter your App Key
app.whenReady().then(() => {
// ... the rest of your app initialization code
});
```
Afterward, you can start tracking events with `trackEvent`:
```js
import { trackEvent } from "@aptabase/electron";
trackEvent("app_started"); // An event with no properties
trackEvent("screen_view", { name: "Settings" }); // An event with a custom property
```
**NOTE:** The `trackEvent` function is available under separate import paths, depending on where you want to track the event from.
- import from `@aptabase/electron` to track events from the `renderer` process
- import from `@aptabase/electron/main` to track events from the `main` process
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 for the `trackEvent` function, it'll run in the background.
4. Only strings and numbers values are allowed on custom properties

View file

@ -0,0 +1,21 @@
import { initialize, trackEvent } from "@aptabase/electron/main";
import { BrowserWindow, app } from "electron";
initialize("A-DEV-7523634193");
app.whenReady().then(() => {
trackEvent("app_started");
const win = new BrowserWindow({
title: "Main window",
});
// You can use `process.env.VITE_DEV_SERVER_URL` when the vite command is called `serve`
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
win.webContents.openDevTools();
} else {
// Load your file
win.loadFile("dist/index.html");
}
});

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1417
packages/electron/example/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
{
"name": "example",
"private": true,
"version": "0.0.0",
"main": "dist-electron/main.js",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"@aptabase/electron": "file:..",
"electron": "12.2.3",
"typescript": "5.0.4",
"vite": "4.3.8",
"vite-plugin-electron": "0.11.2"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,15 @@
import { trackEvent } from "@aptabase/electron";
export function setupCounter(element: HTMLButtonElement) {
let counter = 0;
const setCounter = (count: number) => {
counter = count;
element.innerHTML = `count is ${counter}`;
};
setCounter(0);
element.addEventListener("click", () => {
setCounter(counter + 1);
trackEvent("increment", { counter });
});
}

View file

@ -0,0 +1,24 @@
import { setupCounter } from "./counter";
import "./style.css";
import typescriptLogo from "./typescript.svg";
import viteLogo from "/vite.svg";
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://www.typescriptlang.org/" target="_blank">
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
</a>
<h1>Vite + TypeScript</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite and TypeScript logos to learn more
</p>
</div>
`;
setupCounter(document.querySelector<HTMLButtonElement>("#counter")!);

View file

@ -0,0 +1,97 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #3178c6aa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true
}
}

View file

@ -0,0 +1,12 @@
import { defineConfig } from "vite";
import electron from "vite-plugin-electron";
export default defineConfig({
plugins: [
electron([
{
entry: "electron/main.ts",
},
]),
],
});

View file

@ -0,0 +1,54 @@
{
"name": "@aptabase/electron",
"version": "0.2.2",
"private": false,
"description": "Electron SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps",
"sideEffects": false,
"exports": {
"./package.json": "./package.json",
".": {
"import": "./dist/index.es.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
},
"./main": {
"import": "./dist/main.es.js",
"require": "./dist/main.cjs.js",
"types": "./dist/main.d.ts"
}
},
"typesVersions": {
"*": {
"*": [
"dist/*"
]
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/aptabase/aptabase-electron.git"
},
"bugs": {
"url": "https://github.com/aptabase/aptabase-electron/issues"
},
"homepage": "https://github.com/aptabase/aptabase-electron",
"license": "MIT",
"scripts": {
"build": "vite build"
},
"files": [
"README.md",
"LICENSE",
"dist",
"package.json"
],
"devDependencies": {
"@rollup/plugin-replace": "5.0.2",
"vite": "4.3.9",
"vite-plugin-dts": "2.3.0",
"electron": ">= 8.x"
},
"peerDependencies": {
"electron": ">= 8.x"
}
}

View file

@ -0,0 +1,97 @@
import { exec } from "child_process";
import type { App } from "electron";
import { readFile } from "fs";
import { release } from "os";
// env.PKG_VERSION is replaced by Vite during build phase
const sdkVersion = "aptabase-electron@env.PKG_VERSION";
export interface EnvironmentInfo {
isDebug: boolean;
locale: string;
appVersion: string;
sdkVersion: string;
osName: String;
osVersion: String;
engineName: String;
engineVersion: String;
}
export async function getEnvironmentInfo(app: App): Promise<EnvironmentInfo> {
const [osName, osVersion] = await getOperatingSystem();
return {
appVersion: app.getVersion(),
isDebug: !app.isPackaged,
locale: app.getLocale(),
osName,
osVersion,
engineName: "Chromium",
engineVersion: process.versions.chrome,
sdkVersion,
};
}
async function getOperatingSystem(): Promise<[string, string]> {
switch (process.platform) {
case "win32":
return ["Windows", release()];
case "darwin":
const macOSVersion = await getMacOSVersion();
return ["macOS", macOSVersion];
default:
return await getLinuxInfo();
}
}
async function getMacOSVersion() {
try {
const output = await new Promise<string>((resolve, reject) => {
exec(
"/usr/bin/sw_vers -productVersion",
(error: Error | null, stdout: string) => {
if (error) {
reject(error);
return;
}
resolve(stdout);
}
);
});
return output.trim();
} catch (ex) {
return "";
}
}
async function getLinuxInfo(): Promise<[string, string]> {
try {
const content = await new Promise<string>((resolve, reject) => {
readFile(
"/etc/os-release",
"utf8",
(error: Error | null, output: string) => {
if (error) {
reject(error);
return;
}
resolve(output);
}
);
});
const lines = content.split("\n");
const osData: Record<string, string> = {};
for (const line of lines) {
const [key, value] = line.split("=");
if (key && value) {
osData[key] = value.replace(/"/g, ""); // Remove quotes if present
}
}
const osName = osData["NAME"] ?? "Linux";
const osVersion = osData["VERSION_ID"] ?? "";
return [osName, osVersion];
} catch {
return ["Linux", ""];
}
}

View file

@ -0,0 +1,20 @@
export async function trackEvent(
eventName: string,
props?: Record<string, string | number | boolean>
) {
if (typeof window === "undefined") {
console.error(
"Aptabase: to track events in the main process you must import 'trackEvent' from '@aptabase/electron/main'."
);
return;
}
try {
await fetch("aptabase-ipc://trackEvent", {
method: "POST",
body: JSON.stringify({ eventName, props }),
});
} catch (err) {
console.error("Aptabase: Failed to send event", err);
}
}

View file

@ -0,0 +1,180 @@
import { app, net, protocol } from "electron";
import { EnvironmentInfo, getEnvironmentInfo } from "./env";
import { newSessionId } from "./session";
export type AptabaseOptions = {
host?: string;
};
// Session expires after 1 hour of inactivity
const SESSION_TIMEOUT = 1 * 60 * 60;
let _sessionId = newSessionId();
let _lastTouched = new Date();
let _appKey = "";
let _apiUrl = "";
let _env: EnvironmentInfo | undefined;
const _hosts: { [region: string]: string } = {
US: "https://us.aptabase.com",
EU: "https://eu.aptabase.com",
DEV: "http://localhost:3000",
SH: "",
};
export async function initialize(
appKey: string,
options?: AptabaseOptions
): Promise<void> {
if (app.isReady()) {
console.warn(
"Aptabase: `initialize` must be invoked before the app is ready. Tracking will be disabled."
);
return;
}
const parts = appKey.split("-");
if (parts.length !== 3 || _hosts[parts[1]] === undefined) {
console.warn(
`Aptabase: App Key "${appKey}" is invalid. Tracking will be disabled.`
);
return;
}
registerAptabaseProtocol();
await app.whenReady();
registerEventHandler();
const baseUrl = getBaseUrl(parts[1], options);
_apiUrl = `${baseUrl}/api/v0/event`;
_env = await getEnvironmentInfo(app);
_appKey = appKey;
// some events might be emitted before the initialization is complete
// so we drain the buffer here
drainBuffer();
}
const buffer: {
eventName: string;
props?: Record<string, string | number | boolean>;
}[] = [];
export function trackEvent(
eventName: string,
props?: Record<string, string | number | boolean>
): Promise<void> {
if (!_appKey || !_env) {
buffer.push({ eventName, props });
return Promise.resolve();
}
let now = new Date();
const diffInMs = now.getTime() - _lastTouched.getTime();
const diffInSec = Math.floor(diffInMs / 1000);
if (diffInSec > SESSION_TIMEOUT) {
_sessionId = newSessionId();
}
_lastTouched = now;
const body = {
timestamp: now.toISOString(),
sessionId: _sessionId,
eventName: eventName,
systemProps: {
isDebug: _env.isDebug,
locale: _env.locale,
osName: _env.osName,
osVersion: _env.osVersion,
engineName: _env.engineName,
engineVersion: _env.engineVersion,
appVersion: _env.appVersion,
sdkVersion: _env.sdkVersion,
},
props: props,
};
return new Promise((resolve) => {
const onReject = (err: Error) => {
console.error(`Aptabase: Failed to send event`, err);
resolve();
};
const req = net.request({
method: "POST",
url: _apiUrl,
credentials: "omit",
});
req.setHeader("Content-Type", "application/json");
req.setHeader("App-Key", _appKey);
req.on("error", onReject);
req.on("abort", onReject);
req.on("response", (res) => {
if (res.statusCode >= 300) {
console.warn(
`Aptabase: Failed to send event "${eventName}": ${res.statusCode} ${res.statusMessage}`
);
}
resolve();
});
req.write(JSON.stringify(body));
req.end();
});
}
function drainBuffer() {
while (buffer.length > 0) {
const data = buffer.shift();
if (data) {
trackEvent(data.eventName, data.props);
}
}
}
function registerAptabaseProtocol() {
protocol.registerSchemesAsPrivileged([
{
scheme: "aptabase-ipc",
privileges: {
bypassCSP: true,
corsEnabled: true,
supportFetchAPI: true,
secure: true,
},
},
]);
}
function registerEventHandler() {
protocol.registerStringProtocol("aptabase-ipc", (request, callback) => {
try {
const data = request.uploadData?.[0]?.bytes;
const { eventName, props } = JSON.parse(data?.toString() ?? "{}");
trackEvent(eventName, props);
} catch (err) {
console.error("Aptabase: Failed to send event", err);
}
callback("");
});
}
function getBaseUrl(
region: string,
options?: AptabaseOptions
): string | undefined {
if (region === "SH") {
if (!options?.host) {
console.warn(
`Aptabase: Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.`
);
return;
}
return options.host;
}
return _hosts[region];
}

View file

@ -0,0 +1,25 @@
const crypto = require("crypto");
export function newSessionId(): string {
if (crypto && crypto.randomUUID) {
return crypto.randomUUID();
}
return [
randomStr(8),
randomStr(4),
randomStr(4),
randomStr(4),
randomStr(12),
].join("-");
}
const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
function randomStr(len: number) {
let result = "";
for (let i = 0; i < len; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"outDir": "dist",
"strict": true,
"jsx": "preserve",
"allowJs": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom"]
},
"include": ["src/index.ts", "src/main.ts", "*.d.ts"]
}

View file

@ -0,0 +1,28 @@
import replace from "@rollup/plugin-replace";
import path from "path";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import pkg from "./package.json" assert { type: "json" };
export default defineConfig({
build: {
lib: {
formats: ["cjs", "es"],
entry: {
index: path.resolve(__dirname, "src/index.ts"),
main: path.resolve(__dirname, "src/main.ts"),
},
name: "@aptabase/electron",
fileName: (format, entryName) => `${entryName}.${format}.js`,
},
rollupOptions: {
external: ["electron", "os", "fs", "child_process", "crypto"],
},
},
plugins: [
dts(),
replace({
"env.PKG_VERSION": pkg.version,
}),
],
});