remove non-web packages
5
packages/electron/.gitignore
vendored
|
@ -1,5 +0,0 @@
|
|||
node_modules
|
||||
dist
|
||||
example/node_modules
|
||||
example/dist-electron
|
||||
example/dist
|
10
packages/electron/.vscode/settings.json
vendored
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
## 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
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
|
@ -1,51 +0,0 @@
|
|||

|
||||
|
||||
# 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
|
|
@ -1,21 +0,0 @@
|
|||
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");
|
||||
}
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
<!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
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,15 +0,0 @@
|
|||
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 });
|
||||
});
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
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")!);
|
|
@ -1,97 +0,0 @@
|
|||
: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;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 1.4 KiB |
1
packages/electron/example/src/vite-env.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { defineConfig } from "vite";
|
||||
import electron from "vite-plugin-electron";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
electron([
|
||||
{
|
||||
entry: "electron/main.ts",
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
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", ""];
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
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];
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"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"]
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
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,
|
||||
}),
|
||||
],
|
||||
});
|
0
packages/next/.gitkeep
Normal file
21
packages/react-native/.github/workflows/ci.yaml
vendored
|
@ -1,21 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm test
|
6
packages/react-native/.gitignore
vendored
|
@ -1,6 +0,0 @@
|
|||
node_modules
|
||||
dist
|
||||
coverage
|
||||
|
||||
project.xcworkspace
|
||||
.DS_Store
|
4
packages/react-native/.vscode/settings.json
vendored
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
## 0.2.0
|
||||
|
||||
- Automatic flush of events on app exit
|
||||
- Events are now sent in batches to reduce network overhead
|
||||
- While offline, events will be enqueue and sent when the app is back online
|
||||
|
||||
## 0.1.2
|
||||
|
||||
- Added an option to set the appVersion during init
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- Fixed some links on package.json
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial release
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
|
@ -1,49 +0,0 @@
|
|||

|
||||
|
||||
# React Native SDK for Aptabase
|
||||
|
||||
Instrument your React Native or Expo apps 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/react-native
|
||||
```
|
||||
|
||||
## 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 as early as possible, ideally before declaring the `App` component:
|
||||
|
||||
```js
|
||||
import { init } from "@aptabase/react-native";
|
||||
|
||||
init("<YOUR_APP_KEY>"); // 👈 this is where you enter your App Key
|
||||
```
|
||||
|
||||
Afterwards, you can start tracking events with `trackEvent`:
|
||||
|
||||
```js
|
||||
import { trackEvent } from "@aptabase/react-native";
|
||||
|
||||
trackEvent("app_started"); // An event with no properties
|
||||
trackEvent("screen_view", { name: "Settings" }); // An event with a custom property
|
||||
```
|
||||
|
||||
**Note for Expo apps:** Events sent during development while running on Expo Go will not have the `App Version` property because native modules are not available in Expo Go. However, when you build your app and run it on a real device, the `App Version` property will be available. Alternatively, you can also set the `appVersion` during the `init` call so that it's available during development as well.
|
||||
|
||||
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
|
||||
|
||||
## Preparing for Submission to Apple App Store
|
||||
|
||||
When submitting your app to the Apple App Store, you'll need to fill out the `App Privacy` form. You can find all the answers on our [How to fill out the Apple App Privacy when using Aptabase](https://aptabase.com/docs/apple-app-privacy) guide.
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
def safeExtGet(prop, fallback) {
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 27)
|
||||
buildToolsVersion safeExtGet('buildToolsVersion', '27.0.3')
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet('minSdkVersion', 16)
|
||||
targetSdkVersion safeExtGet('targetSdkVersion', 27)
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
warning 'InvalidPackage'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.facebook.react:react-native:+'
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.aptabase.aptabase">
|
||||
</manifest>
|
|
@ -1,44 +0,0 @@
|
|||
package com.aptabase.aptabase;
|
||||
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class RNAptabaseModule extends ReactContextBaseJavaModule {
|
||||
|
||||
private final ReactApplicationContext reactContext;
|
||||
|
||||
public RNAptabaseModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "RNAptabaseModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
final Map<String, Object> constants = new HashMap<>();
|
||||
final PackageManager packageManager = this.reactContext.getPackageManager();
|
||||
final String packageName = this.reactContext.getPackageName();
|
||||
try {
|
||||
constants.put("appVersion", packageManager.getPackageInfo(packageName, 0).versionName);
|
||||
constants.put("appBuildNumber", packageManager.getPackageInfo(packageName, 0).versionCode);
|
||||
} catch (NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return constants;
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package com.aptabase.aptabase;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class RNAptabasePackage implements ReactPackage {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new RNAptabaseModule(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
require 'json'
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "aptabase-react-native"
|
||||
s.version = package['version']
|
||||
s.summary = package['description']
|
||||
s.license = package['license']
|
||||
|
||||
s.authors = package['author']
|
||||
s.homepage = package['homepage']
|
||||
s.platform = :ios, "10.0"
|
||||
|
||||
s.source = { :git => "https://github.com/demchenkoalex/react-native-module-template.git", :tag => "v#{s.version}" }
|
||||
s.source_files = "ios/**/*.{h,m,swift}"
|
||||
|
||||
s.dependency 'React'
|
||||
end
|
|
@ -1,35 +0,0 @@
|
|||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
|
@ -1,29 +0,0 @@
|
|||
import { StatusBar } from "expo-status-bar";
|
||||
import { Button, StyleSheet, Text, View } from "react-native";
|
||||
import { init, trackEvent } from "@aptabase/react-native";
|
||||
|
||||
init("A-DEV-0000000000");
|
||||
trackEvent("app_started");
|
||||
|
||||
export default function App() {
|
||||
const onClick = () => {
|
||||
trackEvent("Hello");
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Open up App.js to start working on your app!</Text>
|
||||
<Button onPress={onClick} title="Click Me" color="#841584" />
|
||||
<StatusBar style="auto" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"expo": {
|
||||
"name": "HelloWorldExpo",
|
||||
"slug": "HelloWorldExpo",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 46 KiB |
|
@ -1,6 +0,0 @@
|
|||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
13299
packages/react-native/examples/HelloWorldExpo/package-lock.json
generated
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"name": "helloworldexpo",
|
||||
"version": "1.0.0",
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aptabase/react-native": "file:../..",
|
||||
"expo": "~49.0.7",
|
||||
"expo-status-bar": "~1.6.0",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
#import <React/RCTBridgeModule.h>
|
|
@ -1,4 +0,0 @@
|
|||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(RNAptabaseModule, NSObject)
|
||||
@end
|
|
@ -1,17 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
@objc(RNAptabaseModule)
|
||||
class RNAptabaseModule: NSObject {
|
||||
@objc
|
||||
func constantsToExport() -> [AnyHashable : Any]! {
|
||||
return [
|
||||
"appVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as Any,
|
||||
"appBuildNumber": Bundle.main.infoDictionary?["CFBundleVersion"] as Any
|
||||
]
|
||||
}
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,314 +0,0 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
FA4F9FE82512AA42002DB4D5 /* RNAptabaseModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4F9FE72512AA42002DB4D5 /* RNAptabaseModule.swift */; };
|
||||
FA4F9FEB2512ACC2002DB4D5 /* RNAptabaseModule.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4F9FEA2512ACC2002DB4D5 /* RNAptabaseModule.m */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
FA0EFF5E236CC8FB00069FA8 /* CopyFiles */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "include/$(PRODUCT_NAME)";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
FA0EFF60236CC8FB00069FA8 /* libRNAptabaseModule.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNAptabaseModule.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FA4F9FE62512AA41002DB4D5 /* RNAptabaseModule-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNAptabaseModule-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
FA4F9FE72512AA42002DB4D5 /* RNAptabaseModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNAptabaseModule.swift; sourceTree = "<group>"; };
|
||||
FA4F9FEA2512ACC2002DB4D5 /* RNAptabaseModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNAptabaseModule.m; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
FA0EFF5D236CC8FB00069FA8 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
FA0EFF57236CC8FB00069FA8 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FA4F9FE62512AA41002DB4D5 /* RNAptabaseModule-Bridging-Header.h */,
|
||||
FA4F9FEA2512ACC2002DB4D5 /* RNAptabaseModule.m */,
|
||||
FA4F9FE72512AA42002DB4D5 /* RNAptabaseModule.swift */,
|
||||
FA0EFF61236CC8FB00069FA8 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FA0EFF61236CC8FB00069FA8 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FA0EFF60236CC8FB00069FA8 /* libRNAptabaseModule.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
FA0EFF5F236CC8FB00069FA8 /* RNAptabaseModule */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = FA0EFF69236CC8FB00069FA8 /* Build configuration list for PBXNativeTarget "RNAptabaseModule" */;
|
||||
buildPhases = (
|
||||
FA0EFF5C236CC8FB00069FA8 /* Sources */,
|
||||
FA0EFF5D236CC8FB00069FA8 /* Frameworks */,
|
||||
FA0EFF5E236CC8FB00069FA8 /* CopyFiles */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = RNAptabaseModule;
|
||||
productName = RNAptabaseModule;
|
||||
productReference = FA0EFF60236CC8FB00069FA8 /* libRNAptabaseModule.a */;
|
||||
productType = "com.apple.product-type.library.static";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
FA0EFF58236CC8FB00069FA8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1240;
|
||||
ORGANIZATIONNAME = "goenning";
|
||||
TargetAttributes = {
|
||||
FA0EFF5F236CC8FB00069FA8 = {
|
||||
CreatedOnToolsVersion = 11.1;
|
||||
LastSwiftMigration = 1170;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = FA0EFF5B236CC8FB00069FA8 /* Build configuration list for PBXProject "RNAptabaseModule" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = FA0EFF57236CC8FB00069FA8;
|
||||
productRefGroup = FA0EFF61236CC8FB00069FA8 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
FA0EFF5F236CC8FB00069FA8 /* RNAptabaseModule */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
FA0EFF5C236CC8FB00069FA8 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FA4F9FE82512AA42002DB4D5 /* RNAptabaseModule.swift in Sources */,
|
||||
FA4F9FEB2512ACC2002DB4D5 /* RNAptabaseModule.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
FA0EFF67236CC8FB00069FA8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
FA0EFF68236CC8FB00069FA8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
FA0EFF6A236CC8FB00069FA8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../example/ios/Pods/Headers/Public/React-Core",
|
||||
"$(SRCROOT)/../../../ios/Pods/Headers/Public/React-Core",
|
||||
);
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "RNAptabaseModule-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
FA0EFF6B236CC8FB00069FA8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../example/ios/Pods/Headers/Public/React-Core",
|
||||
"$(SRCROOT)/../../../ios/Pods/Headers/Public/React-Core",
|
||||
);
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "RNAptabaseModule-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
FA0EFF5B236CC8FB00069FA8 /* Build configuration list for PBXProject "RNAptabaseModule" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
FA0EFF67236CC8FB00069FA8 /* Debug */,
|
||||
FA0EFF68236CC8FB00069FA8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
FA0EFF69236CC8FB00069FA8 /* Build configuration list for PBXNativeTarget "RNAptabaseModule" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
FA0EFF6A236CC8FB00069FA8 /* Debug */,
|
||||
FA0EFF6B236CC8FB00069FA8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = FA0EFF58236CC8FB00069FA8 /* Project object */;
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1240"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "FA0EFF5F236CC8FB00069FA8"
|
||||
BuildableName = "libRNAptabaseModule.a"
|
||||
BlueprintName = "RNAptabaseModule"
|
||||
ReferencedContainer = "container:RNAptabaseModule.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "FA0EFF5F236CC8FB00069FA8"
|
||||
BuildableName = "libRNAptabaseModule.a"
|
||||
BlueprintName = "RNAptabaseModule"
|
||||
ReferencedContainer = "container:RNAptabaseModule.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"name": "@aptabase/react-native",
|
||||
"version": "0.2.0",
|
||||
"private": false,
|
||||
"description": "React Native SDK for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps",
|
||||
"sideEffects": false,
|
||||
"author": "goenning <goenning@aptabase.com>",
|
||||
"main": "./dist/index.cjs.js",
|
||||
"module": "./dist/index.es.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs.js",
|
||||
"import": "./dist/index.es.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/aptabase/aptabase-react-native.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/aptabase/aptabase-react-native/issues"
|
||||
},
|
||||
"homepage": "https://github.com/aptabase/aptabase-react-native",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run --coverage"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"dist",
|
||||
"package.json",
|
||||
"android",
|
||||
"ios",
|
||||
"aptabase-react-native.podspec"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@vitest/coverage-v8": "0.34.2",
|
||||
"vite": "4.4.9",
|
||||
"vite-plugin-dts": "3.5.2",
|
||||
"vitest": "0.34.2",
|
||||
"vitest-fetch-mock": "0.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import createFetchMock from "vitest-fetch-mock";
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.stubGlobal("__DEV__", true);
|
||||
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
|
||||
fetchMocker.enableMocks();
|
|
@ -1,122 +0,0 @@
|
|||
import "vitest-fetch-mock";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AptabaseClient } from "./client";
|
||||
import type { EnvironmentInfo } from "./env";
|
||||
|
||||
const env: EnvironmentInfo = {
|
||||
isDebug: false,
|
||||
locale: "en-US",
|
||||
osName: "iOS",
|
||||
osVersion: "14.3",
|
||||
appVersion: "1.0.0",
|
||||
appBuildNumber: "1",
|
||||
sdkVersion: "aptabase-reactnative@1.0.0",
|
||||
};
|
||||
|
||||
describe("AptabaseClient", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
fetchMock.resetMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should allow override of appVersion", async () => {
|
||||
const client = new AptabaseClient("A-DEV-000", env, {
|
||||
appVersion: "2.0.0",
|
||||
});
|
||||
|
||||
client.trackEvent("Hello");
|
||||
await client.flush();
|
||||
|
||||
const body = await fetchMock.requests().at(0)?.json();
|
||||
expect(body[0].eventName).toEqual("Hello");
|
||||
expect(body[0].systemProps).toEqual({ ...env, appVersion: "2.0.0" });
|
||||
});
|
||||
|
||||
it("should send event with correct props", async () => {
|
||||
const client = new AptabaseClient("A-DEV-000", env);
|
||||
|
||||
client.trackEvent("test", { count: 1, foo: "bar" });
|
||||
await client.flush();
|
||||
|
||||
const body = await fetchMock.requests().at(0)?.json();
|
||||
expect(body[0].eventName).toEqual("test");
|
||||
expect(body[0].props).toEqual({ count: 1, foo: "bar" });
|
||||
expect(body[0].systemProps).toEqual(env);
|
||||
});
|
||||
|
||||
it("should flush events every 500ms", async () => {
|
||||
const client = new AptabaseClient("A-DEV-000", env);
|
||||
client.startPolling(500);
|
||||
|
||||
client.trackEvent("Hello1");
|
||||
vi.advanceTimersByTime(510);
|
||||
|
||||
expect(fetchMock.requests().length).toEqual(1);
|
||||
const request1 = await fetchMock.requests().at(0)?.json();
|
||||
expect(request1[0].eventName).toEqual("Hello1");
|
||||
|
||||
// after another tick, nothing should be sent
|
||||
vi.advanceTimersByTime(510);
|
||||
expect(fetchMock.requests().length).toEqual(1);
|
||||
|
||||
// after a trackEvent and another tick, the event should be sent
|
||||
client.trackEvent("Hello2");
|
||||
vi.advanceTimersByTime(510);
|
||||
expect(fetchMock.requests().length).toEqual(2);
|
||||
const request2 = await fetchMock.requests().at(1)?.json();
|
||||
expect(request2[0].eventName).toEqual("Hello2");
|
||||
});
|
||||
|
||||
it("should stop flush if polling stopped", async () => {
|
||||
const client = new AptabaseClient("A-DEV-000", env);
|
||||
client.startPolling(500);
|
||||
|
||||
client.trackEvent("Hello1");
|
||||
vi.advanceTimersByTime(510);
|
||||
|
||||
expect(fetchMock.requests().length).toEqual(1);
|
||||
|
||||
// if polling stopped, no more events should be sent
|
||||
client.stopPolling();
|
||||
client.trackEvent("Hello2");
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(fetchMock.requests().length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should generate new session after long period of inactivity", async () => {
|
||||
const client = new AptabaseClient("A-DEV-000", env);
|
||||
|
||||
client.trackEvent("Hello1");
|
||||
await client.flush();
|
||||
|
||||
const request1 = await fetchMock.requests().at(0)?.json();
|
||||
const sessionId1 = request1[0].sessionId;
|
||||
expect(sessionId1).toBeDefined();
|
||||
|
||||
// after 10 minutes, the same session should be used
|
||||
vi.advanceTimersByTime(10 * 60 * 1000);
|
||||
|
||||
client.trackEvent("Hello2");
|
||||
await client.flush();
|
||||
|
||||
const request2 = await fetchMock.requests().at(1)?.json();
|
||||
const sessionId2 = request2[0].sessionId;
|
||||
expect(sessionId2).toBeDefined();
|
||||
expect(sessionId2).toBe(sessionId1);
|
||||
|
||||
// after 2 hours, the same session should be used
|
||||
vi.advanceTimersByTime(2 * 60 * 60 * 1000);
|
||||
|
||||
client.trackEvent("Hello3");
|
||||
await client.flush();
|
||||
|
||||
const request3 = await fetchMock.requests().at(2)?.json();
|
||||
const sessionId3 = request3[0].sessionId;
|
||||
expect(sessionId3).toBeDefined();
|
||||
expect(sessionId3).not.toBe(sessionId1);
|
||||
});
|
||||
});
|
|
@ -1,83 +0,0 @@
|
|||
import type { Platform } from "react-native";
|
||||
import type { AptabaseOptions } from "./types";
|
||||
import type { EnvironmentInfo } from "./env";
|
||||
import { EventDispatcher } from "./dispatcher";
|
||||
import { newSessionId } from "./session";
|
||||
import { HOSTS, SESSION_TIMEOUT } from "./constants";
|
||||
|
||||
export class AptabaseClient {
|
||||
private readonly _dispatcher: EventDispatcher;
|
||||
private readonly _env: EnvironmentInfo;
|
||||
private _sessionId = newSessionId();
|
||||
private _lastTouched = new Date();
|
||||
private _flushTimer: number | undefined;
|
||||
|
||||
constructor(appKey: string, env: EnvironmentInfo, options?: AptabaseOptions) {
|
||||
const [_, region] = appKey.split("-");
|
||||
const baseUrl = this.getBaseUrl(region, options);
|
||||
|
||||
this._env = { ...env };
|
||||
if (options?.appVersion) {
|
||||
this._env.appVersion = options.appVersion;
|
||||
}
|
||||
|
||||
this._dispatcher = new EventDispatcher(appKey, baseUrl, env);
|
||||
}
|
||||
|
||||
public trackEvent(
|
||||
eventName: string,
|
||||
props?: Record<string, string | number | boolean>
|
||||
) {
|
||||
this._dispatcher.enqueue({
|
||||
timestamp: new Date().toISOString(),
|
||||
sessionId: this.evalSessionId(),
|
||||
eventName: eventName,
|
||||
systemProps: {
|
||||
isDebug: this._env.isDebug,
|
||||
locale: this._env.locale,
|
||||
osName: this._env.osName,
|
||||
osVersion: this._env.osVersion,
|
||||
appVersion: this._env.appVersion,
|
||||
appBuildNumber: this._env.appBuildNumber,
|
||||
sdkVersion: this._env.sdkVersion,
|
||||
},
|
||||
props: props,
|
||||
});
|
||||
}
|
||||
|
||||
public startPolling(flushInterval: number) {
|
||||
this.stopPolling();
|
||||
|
||||
this._flushTimer = setInterval(this.flush.bind(this), flushInterval);
|
||||
}
|
||||
|
||||
public stopPolling() {
|
||||
if (this._flushTimer) {
|
||||
clearInterval(this._flushTimer);
|
||||
this._flushTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public flush(): Promise<void> {
|
||||
return this._dispatcher.flush();
|
||||
}
|
||||
|
||||
private evalSessionId() {
|
||||
let now = new Date();
|
||||
const diffInMs = now.getTime() - this._lastTouched.getTime();
|
||||
if (diffInMs > SESSION_TIMEOUT) {
|
||||
this._sessionId = newSessionId();
|
||||
}
|
||||
this._lastTouched = now;
|
||||
|
||||
return this._sessionId;
|
||||
}
|
||||
|
||||
private getBaseUrl(region: string, options?: AptabaseOptions): string {
|
||||
if (region === "SH") {
|
||||
return options?.host ?? HOSTS.DEV;
|
||||
}
|
||||
|
||||
return HOSTS[region];
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// Session expires after 1 hour of inactivity
|
||||
export const SESSION_TIMEOUT = 60 * 60 * 1000;
|
||||
|
||||
// Flush events every 60 seconds in production, or 2 seconds in development
|
||||
export const FLUSH_INTERVAL = __DEV__ ? 2000 : 60000;
|
||||
|
||||
// List of hosts for each region
|
||||
// To use a self-hosted (SH) deployment, the host must be set during init
|
||||
export const HOSTS: { [region: string]: string } = {
|
||||
US: "https://us.aptabase.com",
|
||||
EU: "https://eu.aptabase.com",
|
||||
DEV: "http://localhost:3000",
|
||||
SH: "",
|
||||
};
|
|
@ -1,140 +0,0 @@
|
|||
import "vitest-fetch-mock";
|
||||
import { EventDispatcher } from "./dispatcher";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { EnvironmentInfo } from "./env";
|
||||
|
||||
const env: EnvironmentInfo = {
|
||||
isDebug: false,
|
||||
locale: "en-US",
|
||||
osName: "iOS",
|
||||
osVersion: "14.3",
|
||||
appVersion: "1.0.0",
|
||||
appBuildNumber: "1",
|
||||
sdkVersion: "aptabase-reactnative@1.0.0",
|
||||
};
|
||||
|
||||
const createEvent = (eventName: string) => ({
|
||||
timestamp: new Date().toISOString(),
|
||||
sessionId: "123",
|
||||
eventName,
|
||||
systemProps: { ...env },
|
||||
});
|
||||
|
||||
const expectRequestCount = (count: number) => {
|
||||
expect(fetchMock.requests().length).toEqual(count);
|
||||
};
|
||||
|
||||
const expectEventsCount = async (
|
||||
requestIndex: number,
|
||||
expectedNumOfEvents: number
|
||||
) => {
|
||||
const body = await fetchMock.requests().at(requestIndex)?.json();
|
||||
expect(body.length).toEqual(expectedNumOfEvents);
|
||||
};
|
||||
|
||||
describe("EventDispatcher", () => {
|
||||
let dispatcher: EventDispatcher;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatcher = new EventDispatcher(
|
||||
"A-DEV-000",
|
||||
"https://localhost:3000",
|
||||
env
|
||||
);
|
||||
fetchMock.resetMocks();
|
||||
});
|
||||
|
||||
it("should not send a request if queue is empty", async () => {
|
||||
await dispatcher.flush();
|
||||
|
||||
expectRequestCount(0);
|
||||
});
|
||||
|
||||
it("should send even with correct headers", async () => {
|
||||
dispatcher.enqueue(createEvent("app_started"));
|
||||
await dispatcher.flush();
|
||||
|
||||
const request = await fetchMock.requests().at(0);
|
||||
expect(request).not.toBeUndefined();
|
||||
expect(request?.url).toEqual("https://localhost:3000/api/v0/events");
|
||||
expect(request?.headers.get("Content-Type")).toEqual("application/json");
|
||||
expect(request?.headers.get("App-Key")).toEqual("A-DEV-000");
|
||||
expect(request?.headers.get("User-Agent")).toEqual("iOS/14.3 en-US");
|
||||
});
|
||||
|
||||
it("should dispatch single event", async () => {
|
||||
fetchMock.mockResponseOnce("{}");
|
||||
|
||||
dispatcher.enqueue(createEvent("app_started"));
|
||||
await dispatcher.flush();
|
||||
|
||||
expectRequestCount(1);
|
||||
await expectEventsCount(0, 1);
|
||||
});
|
||||
|
||||
it("should not dispatch event if it's already been sent", async () => {
|
||||
fetchMock.mockResponseOnce("{}");
|
||||
|
||||
dispatcher.enqueue(createEvent("app_started"));
|
||||
await dispatcher.flush();
|
||||
expectRequestCount(1);
|
||||
|
||||
await dispatcher.flush();
|
||||
expectRequestCount(1);
|
||||
});
|
||||
|
||||
it("should dispatch multiple events", async () => {
|
||||
fetchMock.mockResponseOnce("{}");
|
||||
|
||||
dispatcher.enqueue(createEvent("app_started"));
|
||||
dispatcher.enqueue(createEvent("app_exited"));
|
||||
await dispatcher.flush();
|
||||
|
||||
expectRequestCount(1);
|
||||
await expectEventsCount(0, 2);
|
||||
});
|
||||
|
||||
it("should send many events in chunks of 25 items", async () => {
|
||||
fetchMock.mockResponseOnce("{}");
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
dispatcher.enqueue(createEvent("hello_world"));
|
||||
}
|
||||
await dispatcher.flush();
|
||||
|
||||
expectRequestCount(3);
|
||||
await expectEventsCount(0, 25);
|
||||
await expectEventsCount(1, 25);
|
||||
await expectEventsCount(2, 10);
|
||||
});
|
||||
|
||||
it("should retry failed requests in a subsequent flush", async () => {
|
||||
fetchMock.mockResponseOnce("{}", { status: 500 });
|
||||
|
||||
dispatcher.enqueue(createEvent("hello_world"));
|
||||
await dispatcher.flush();
|
||||
|
||||
expectRequestCount(1);
|
||||
await expectEventsCount(0, 1);
|
||||
|
||||
fetchMock.mockResponseOnce("{}", { status: 200 });
|
||||
await dispatcher.flush();
|
||||
|
||||
expectRequestCount(2);
|
||||
await expectEventsCount(1, 1);
|
||||
});
|
||||
|
||||
it("should not retry requests that failed with 4xx", async () => {
|
||||
fetchMock.mockResponseOnce("{}", { status: 400 });
|
||||
|
||||
dispatcher.enqueue(createEvent("hello_world"));
|
||||
await dispatcher.flush();
|
||||
|
||||
expectRequestCount(1);
|
||||
await expectEventsCount(0, 1);
|
||||
|
||||
await dispatcher.flush();
|
||||
|
||||
expectRequestCount(1);
|
||||
});
|
||||
});
|
|
@ -1,77 +0,0 @@
|
|||
import type { Event } from "./types";
|
||||
import { EnvironmentInfo } from "./env";
|
||||
|
||||
export class EventDispatcher {
|
||||
private _events: Event[] = [];
|
||||
private MAX_BATCH_SIZE = 25;
|
||||
private headers: Headers;
|
||||
private apiUrl: string;
|
||||
|
||||
constructor(appKey: string, baseUrl: string, env: EnvironmentInfo) {
|
||||
this.apiUrl = `${baseUrl}/api/v0/events`;
|
||||
this.headers = new Headers({
|
||||
"Content-Type": "application/json",
|
||||
"App-Key": appKey,
|
||||
"User-Agent": `${env.osName}/${env.osVersion} ${env.locale}`,
|
||||
});
|
||||
}
|
||||
|
||||
public enqueue(evt: Event | Event[]) {
|
||||
if (Array.isArray(evt)) {
|
||||
this._events.push(...evt);
|
||||
return;
|
||||
}
|
||||
|
||||
this._events.push(evt);
|
||||
}
|
||||
|
||||
public async flush(): Promise<void> {
|
||||
if (this._events.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let failedEvents: Event[] = [];
|
||||
do {
|
||||
const eventsToSend = this._events.splice(0, this.MAX_BATCH_SIZE);
|
||||
try {
|
||||
await this._sendEvents(eventsToSend);
|
||||
} catch {
|
||||
failedEvents = [...failedEvents, ...eventsToSend];
|
||||
}
|
||||
} while (this._events.length > 0);
|
||||
|
||||
if (failedEvents.length > 0) {
|
||||
this.enqueue(failedEvents);
|
||||
}
|
||||
}
|
||||
|
||||
private async _sendEvents(events: Event[]): Promise<void> {
|
||||
try {
|
||||
const res = await fetch(this.apiUrl, {
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
credentials: "omit",
|
||||
body: JSON.stringify(events),
|
||||
});
|
||||
|
||||
if (res.status < 300) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const reason = `${res.status} ${await res.text()}`;
|
||||
if (res.status < 500) {
|
||||
console.warn(
|
||||
`Aptabase: Failed to send ${events.length} events because of ${reason}. Will not retry.`
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
throw new Error(reason);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Aptabase: Failed to send ${events.length} events. Reason: ${e}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { Platform } from "react-native";
|
||||
import version from "./version";
|
||||
|
||||
// env.PKG_VERSION is replaced by Vite during build phase
|
||||
const sdkVersion = "aptabase-reactnative@env.PKG_VERSION";
|
||||
|
||||
export interface EnvironmentInfo {
|
||||
isDebug: boolean;
|
||||
locale: string;
|
||||
appVersion: string;
|
||||
appBuildNumber: string;
|
||||
sdkVersion: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
}
|
||||
|
||||
export function getEnvironmentInfo(): EnvironmentInfo {
|
||||
const [osName, osVersion] = getOperatingSystem();
|
||||
|
||||
const locale = "en-US";
|
||||
|
||||
return {
|
||||
appVersion: version.appVersion || "",
|
||||
appBuildNumber: version.appBuildNumber || "",
|
||||
isDebug: __DEV__,
|
||||
locale,
|
||||
osName,
|
||||
osVersion,
|
||||
sdkVersion,
|
||||
};
|
||||
}
|
||||
|
||||
function getOperatingSystem(): [string, string] {
|
||||
switch (Platform.OS) {
|
||||
case "android":
|
||||
return ["Android", Platform.Version.toString()];
|
||||
case "ios":
|
||||
if (Platform.isPad) {
|
||||
return ["iPadOS", Platform.Version];
|
||||
}
|
||||
return ["iOS", Platform.Version];
|
||||
default:
|
||||
return ["", ""];
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import type { AptabaseOptions } from "./types";
|
||||
import { getEnvironmentInfo } from "./env";
|
||||
import { AppState, Platform } from "react-native";
|
||||
import { AptabaseClient } from "./client";
|
||||
import { FLUSH_INTERVAL } from "./constants";
|
||||
import { validate } from "./validate";
|
||||
|
||||
let _client: AptabaseClient | undefined;
|
||||
|
||||
/**
|
||||
* Initializes the SDK with given App Key
|
||||
* @param {string} appKey - Aptabase App Key
|
||||
* @param {AptabaseOptions} options - Optional initialization parameters
|
||||
*/
|
||||
export function init(appKey: string, options?: AptabaseOptions) {
|
||||
const [ok, msg] = validate(Platform.OS, appKey, options);
|
||||
if (!ok) {
|
||||
console.warn(`Aptabase: ${msg}. Tracking will be disabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const env = getEnvironmentInfo();
|
||||
_client = new AptabaseClient(appKey, env, options);
|
||||
|
||||
const flushInterval = options?.flushInterval ?? FLUSH_INTERVAL;
|
||||
_client.startPolling(flushInterval);
|
||||
|
||||
if (!AppState.isAvailable) return;
|
||||
|
||||
AppState.addEventListener("change", (next) => {
|
||||
_client?.stopPolling();
|
||||
|
||||
switch (next) {
|
||||
case "active":
|
||||
_client?.startPolling(flushInterval);
|
||||
break;
|
||||
|
||||
case "background":
|
||||
_client?.flush();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track an event using given properties
|
||||
* @param {string} eventName - The name of the event to track
|
||||
* @param {Object} props - Optional custom properties
|
||||
*/
|
||||
export function trackEvent(
|
||||
eventName: string,
|
||||
props?: Record<string, string | number | boolean>
|
||||
) {
|
||||
_client?.trackEvent(eventName, props);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { newSessionId } from "./session";
|
||||
|
||||
describe("Session", () => {
|
||||
it("should generate session ids", async () => {
|
||||
const id = newSessionId();
|
||||
|
||||
expect(id).toHaveLength(36);
|
||||
const uuidRegex =
|
||||
/^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/;
|
||||
expect(id).toMatch(uuidRegex);
|
||||
});
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
export function newSessionId() {
|
||||
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;
|
||||
}
|
33
packages/react-native/src/types.d.ts
vendored
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* Custom initialization parameters for Aptabase SDK.
|
||||
* Use this when calling the init function.
|
||||
*/
|
||||
export type AptabaseOptions = {
|
||||
// Host URL for Self-Hosted deployments
|
||||
host?: string;
|
||||
|
||||
// Custom appVersion to override the default
|
||||
appVersion?: string;
|
||||
|
||||
// Override the default flush interval (in milliseconds)
|
||||
flushInterval?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A tracked event instance representing something that happened in the app.
|
||||
*/
|
||||
export type Event = {
|
||||
timestamp: string;
|
||||
sessionId: string;
|
||||
eventName: string;
|
||||
systemProps: {
|
||||
isDebug: boolean;
|
||||
locale: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
appVersion: string;
|
||||
appBuildNumber: string;
|
||||
sdkVersion: string;
|
||||
};
|
||||
props?: Record<string, string | number | boolean>;
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { validate } from "./validate";
|
||||
|
||||
describe("Validate", () => {
|
||||
[
|
||||
{
|
||||
platform: "ios" as const,
|
||||
appKey: "A-DEV-000",
|
||||
options: undefined,
|
||||
expected: [true, ""],
|
||||
},
|
||||
{
|
||||
platform: "ios" as const,
|
||||
appKey: "A-SH-1234",
|
||||
options: {
|
||||
host: "https://aptabase.mycompany.com",
|
||||
},
|
||||
expected: [true, ""],
|
||||
},
|
||||
{
|
||||
platform: "web" as const,
|
||||
appKey: "A-DEV-000",
|
||||
options: undefined,
|
||||
expected: [false, "This SDK is only supported on Android and iOS."],
|
||||
},
|
||||
{
|
||||
platform: "ios" as const,
|
||||
appKey: "A-WTF-000",
|
||||
options: undefined,
|
||||
expected: [false, 'App Key "A-WTF-000" is invalid'],
|
||||
},
|
||||
{
|
||||
platform: "ios" as const,
|
||||
appKey: "A-SH-1234",
|
||||
options: undefined,
|
||||
expected: [
|
||||
false,
|
||||
"Host parameter must be defined when using Self-Hosted App Key.",
|
||||
],
|
||||
},
|
||||
].forEach(({ platform, appKey, options, expected }) => {
|
||||
it(`should validate ${platform} ${appKey} ${options}`, async () => {
|
||||
const result = validate(platform, appKey, options);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
import type { Platform } from "react-native";
|
||||
import { HOSTS } from "./constants";
|
||||
|
||||
import type { AptabaseOptions } from "./types";
|
||||
|
||||
export function validate(
|
||||
platform: typeof Platform.OS,
|
||||
appKey: string,
|
||||
options?: AptabaseOptions
|
||||
): [boolean, string] {
|
||||
if (platform !== "android" && platform !== "ios") {
|
||||
return [false, "This SDK is only supported on Android and iOS."];
|
||||
}
|
||||
|
||||
const parts = appKey.split("-");
|
||||
if (parts.length !== 3 || HOSTS[parts[1]] === undefined) {
|
||||
return [false, `App Key "${appKey}" is invalid`];
|
||||
}
|
||||
|
||||
if (parts[1] === "SH" && !options?.host) {
|
||||
return [
|
||||
false,
|
||||
`Host parameter must be defined when using Self-Hosted App Key.`,
|
||||
];
|
||||
}
|
||||
|
||||
return [true, ""];
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
const { RNAptabaseModule } = NativeModules;
|
||||
|
||||
type VersionObject = {
|
||||
appVersion: string | undefined,
|
||||
appBuildNumber: string | undefined,
|
||||
};
|
||||
|
||||
const Version: VersionObject = {
|
||||
appVersion: RNAptabaseModule && RNAptabaseModule.appVersion,
|
||||
appBuildNumber: RNAptabaseModule && RNAptabaseModule.appBuildNumber,
|
||||
};
|
||||
|
||||
export default Version;
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"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": ["react-native"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/index.ts", "*.d.ts"]
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
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"),
|
||||
},
|
||||
name: "@aptabase/react-native",
|
||||
fileName: (format, entryName) => `${entryName}.${format}.js`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["react", "react-native"],
|
||||
},
|
||||
},
|
||||
test: {
|
||||
setupFiles: ["./setupVitest.ts"],
|
||||
coverage: {
|
||||
reporter: ["lcov", "text"],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts(),
|
||||
replace({
|
||||
"env.PKG_VERSION": pkg.version,
|
||||
}),
|
||||
],
|
||||
});
|
0
packages/remix/.gitkeep
Normal file
14
packages/tauri/.gitignore
vendored
|
@ -1,14 +0,0 @@
|
|||
/.vs
|
||||
.DS_Store
|
||||
.Thumbs.db
|
||||
*.sublime*
|
||||
.idea/
|
||||
debug.log
|
||||
package-lock.json
|
||||
.vscode/settings.json
|
||||
yarn.lock
|
||||
|
||||
/target
|
||||
Cargo.lock
|
||||
node_modules/
|
||||
webview-dist
|
|
@ -1,31 +0,0 @@
|
|||
## 0.4.1
|
||||
|
||||
* Automatic flush of events on app exit
|
||||
* Fix User-Agent header
|
||||
|
||||
## 0.4.0
|
||||
|
||||
* Events are now sent in batches to reduce network overhead
|
||||
* While offline, events will be enqueue and sent when the app is back online
|
||||
* Tauri 1.4 required
|
||||
|
||||
## 0.3.2
|
||||
|
||||
* (macOS) Fixed an issue where sessions could span multiple days if the app was left open overnight
|
||||
|
||||
## 0.3.1
|
||||
|
||||
* Wait for event to be flushed on panic
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Add ability for panic hook to log panics to aptabase
|
||||
|
||||
## 0.2.1
|
||||
|
||||
* Added support for automatic segregation of Debug/Release data source
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* BREAKING CHANGE: replaced the `init` function with a `Builder` struct, see README for example usage
|
||||
* Ability to set custom hosts for self hosted servers
|
|
@ -1,24 +0,0 @@
|
|||
[package]
|
||||
name = "tauri-plugin-aptabase"
|
||||
version = "0.4.1"
|
||||
license = "MIT"
|
||||
description = "Tauri Plugin for Aptabase: Open Source, Privacy-First and Simple Analytics for Mobile, Desktop and Web Apps"
|
||||
authors = [ "Guilherme Oenning" ]
|
||||
edition = "2021"
|
||||
rust-version = "1.59"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/aptabase/tauri-plugin-aptabase"
|
||||
exclude = ["/examples", "/webview-dist", "/webview-src", "node_modules"]
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1", features = ["os-api"] }
|
||||
tokio = "1"
|
||||
futures = "0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
time = { version = "0.3", features = ["formatting"]}
|
||||
os_info = "3"
|
||||
uuid = "1"
|
||||
log = "0.4"
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
|
@ -1,83 +0,0 @@
|
|||

|
||||
|
||||
# Tauri Plugin for Aptabase
|
||||
|
||||
This plugin allows you to instrument your app with events that can be analyzed in Aptabase, an Open Source, Privacy-First, and Simple Analytics for Mobile, Desktop, and Web Apps.
|
||||
|
||||
## Install
|
||||
|
||||
Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
`src-tauri/Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-aptabase = "0.4"
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager
|
||||
|
||||
```bash
|
||||
npm add @aptabase/tauri
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First, you need to get your `App Key` from Aptabase, you can find it in the `Instructions` menu on the left side menu.
|
||||
|
||||
Then you need to register the core plugin with Tauri:
|
||||
|
||||
`src-tauri/src/main.rs`
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_aptabase::Builder::new("<YOUR_APP_KEY>").build()) // 👈 this is where you enter your App Key
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
|
||||
You can then start sending events from Rust by importing the `tauri_plugin_aptabase::EventTracker` trait and calling the `track_event` method on `App`, `AppHandle` or `Window`.
|
||||
|
||||
As an example, you can add `app_started` and `app_exited` events like this:
|
||||
|
||||
|
||||
```rust
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_aptabase::init("<YOUR_APP_KEY>".into()))
|
||||
.setup(|app| {
|
||||
app.track_event("app_started", None);
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application")
|
||||
.run(|handler, event| match event {
|
||||
tauri::RunEvent::Exit { .. } => {
|
||||
handler.track_event("app_exited", None);
|
||||
handler.flush_events_blocking();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `trackEvent` function is also available through the JavaScript guest bindings:
|
||||
|
||||
```js
|
||||
import { trackEvent } from "@aptabase/tauri";
|
||||
|
||||
trackEvent("save_settings") // An event with no properties
|
||||
trackEvent("screen_view", { name: "Settings" }) // An event with a custom property
|
||||
```
|
||||
|
||||
A few important notes:
|
||||
|
||||
1. The plugin 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 plugin 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.
|
||||
3. Only strings and numbers values are allowed on custom properties
|
24
packages/tauri/examples/helloworld/.gitignore
vendored
|
@ -1,24 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
|
@ -1,7 +0,0 @@
|
|||
# Tauri + React + Typescript
|
||||
|
||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
|
@ -1,14 +0,0 @@
|
|||
<!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>Tauri + React + TS</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"name": "helloworld",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"dev:vite": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"@aptabase/tauri": "file:../../"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^4.0.0",
|
||||
"@tauri-apps/cli": "^1.2.2"
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.5 KiB |
|
@ -1 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,4 +0,0 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
[package]
|
||||
name = "helloworld"
|
||||
version = "0.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.4", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.4", features = ["shell-open"] }
|
||||
tauri-plugin-aptabase = { path = "../../../" }
|
||||
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = ["custom-protocol"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 974 B |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 903 B |
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,41 +0,0 @@
|
|||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use tauri_plugin_log::LogTarget;
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
use serde_json::json;
|
||||
|
||||
#[tauri::command]
|
||||
fn this_will_panic() {
|
||||
panic!("I told you!");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![this_will_panic])
|
||||
.plugin(tauri_plugin_aptabase::Builder::new("A-DEV-0000000000").with_panic_hook(Box::new(|client, info| {
|
||||
client.track_event("panic", Some(json!({
|
||||
"info": format!("{:?}", info),
|
||||
})));
|
||||
})).build())
|
||||
.plugin(tauri_plugin_log::Builder::default().targets([
|
||||
LogTarget::LogDir,
|
||||
LogTarget::Stdout,
|
||||
LogTarget::Webview,
|
||||
]).build())
|
||||
.setup(|app| {
|
||||
app.track_event("app_started", None);
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application")
|
||||
.run(|handler, event| match event {
|
||||
tauri::RunEvent::Exit { .. } => {
|
||||
handler.track_event("app_exit", None);
|
||||
handler.flush_events_blocking();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|