refacto/migrate-turborepo
This commit is contained in:
parent
9905366e9a
commit
a9507e7d3d
134 changed files with 20842 additions and 475 deletions
21
packages/react-native/.github/workflows/ci.yaml
vendored
Normal file
21
packages/react-native/.github/workflows/ci.yaml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
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
Normal file
6
packages/react-native/.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
dist
|
||||
coverage
|
||||
|
||||
project.xcworkspace
|
||||
.DS_Store
|
4
packages/react-native/.vscode/settings.json
vendored
Normal file
4
packages/react-native/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2
|
||||
}
|
17
packages/react-native/CHANGELOG.md
Normal file
17
packages/react-native/CHANGELOG.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
## 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
|
21
packages/react-native/LICENSE
Normal file
21
packages/react-native/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Sumbit Labs Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
49
packages/react-native/README.md
Normal file
49
packages/react-native/README.md
Normal file
|
@ -0,0 +1,49 @@
|
|||

|
||||
|
||||
# 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.
|
||||
|
27
packages/react-native/android/build.gradle
Normal file
27
packages/react-native/android/build.gradle
Normal file
|
@ -0,0 +1,27 @@
|
|||
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:+'
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.aptabase.aptabase">
|
||||
</manifest>
|
|
@ -0,0 +1,44 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
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();
|
||||
}
|
||||
}
|
19
packages/react-native/aptabase-react-native.podspec
Normal file
19
packages/react-native/aptabase-react-native.podspec
Normal file
|
@ -0,0 +1,19 @@
|
|||
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
|
35
packages/react-native/examples/HelloWorldExpo/.gitignore
vendored
Normal file
35
packages/react-native/examples/HelloWorldExpo/.gitignore
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
# 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
|
29
packages/react-native/examples/HelloWorldExpo/App.js
vendored
Normal file
29
packages/react-native/examples/HelloWorldExpo/App.js
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
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",
|
||||
},
|
||||
});
|
30
packages/react-native/examples/HelloWorldExpo/app.json
Normal file
30
packages/react-native/examples/HelloWorldExpo/app.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
packages/react-native/examples/HelloWorldExpo/assets/favicon.png
Normal file
BIN
packages/react-native/examples/HelloWorldExpo/assets/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/react-native/examples/HelloWorldExpo/assets/icon.png
Normal file
BIN
packages/react-native/examples/HelloWorldExpo/assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
packages/react-native/examples/HelloWorldExpo/assets/splash.png
Normal file
BIN
packages/react-native/examples/HelloWorldExpo/assets/splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -0,0 +1,6 @@
|
|||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
13299
packages/react-native/examples/HelloWorldExpo/package-lock.json
generated
Normal file
13299
packages/react-native/examples/HelloWorldExpo/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
packages/react-native/examples/HelloWorldExpo/package.json
Normal file
22
packages/react-native/examples/HelloWorldExpo/package.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
#import <React/RCTBridgeModule.h>
|
4
packages/react-native/ios/RNAptabaseModule.m
Normal file
4
packages/react-native/ios/RNAptabaseModule.m
Normal file
|
@ -0,0 +1,4 @@
|
|||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(RNAptabaseModule, NSObject)
|
||||
@end
|
17
packages/react-native/ios/RNAptabaseModule.swift
Normal file
17
packages/react-native/ios/RNAptabaseModule.swift
Normal file
|
@ -0,0 +1,17 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,314 @@
|
|||
// !$*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 */;
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?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>
|
52
packages/react-native/package.json
Normal file
52
packages/react-native/package.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"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": "*"
|
||||
}
|
||||
}
|
8
packages/react-native/setupVitest.ts
Normal file
8
packages/react-native/setupVitest.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import createFetchMock from "vitest-fetch-mock";
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.stubGlobal("__DEV__", true);
|
||||
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
|
||||
fetchMocker.enableMocks();
|
122
packages/react-native/src/client.spec.ts
Normal file
122
packages/react-native/src/client.spec.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
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);
|
||||
});
|
||||
});
|
83
packages/react-native/src/client.ts
Normal file
83
packages/react-native/src/client.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
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];
|
||||
}
|
||||
}
|
14
packages/react-native/src/constants.ts
Normal file
14
packages/react-native/src/constants.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
// 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: "",
|
||||
};
|
140
packages/react-native/src/dispatcher.spec.ts
Normal file
140
packages/react-native/src/dispatcher.spec.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
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);
|
||||
});
|
||||
});
|
77
packages/react-native/src/dispatcher.ts
Normal file
77
packages/react-native/src/dispatcher.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
45
packages/react-native/src/env.ts
Normal file
45
packages/react-native/src/env.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
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 ["", ""];
|
||||
}
|
||||
}
|
55
packages/react-native/src/index.ts
Normal file
55
packages/react-native/src/index.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
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);
|
||||
}
|
13
packages/react-native/src/session.spec.ts
Normal file
13
packages/react-native/src/session.spec.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
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);
|
||||
});
|
||||
});
|
19
packages/react-native/src/session.ts
Normal file
19
packages/react-native/src/session.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
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
Normal file
33
packages/react-native/src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 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>;
|
||||
};
|
47
packages/react-native/src/validate.spec.ts
Normal file
47
packages/react-native/src/validate.spec.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
28
packages/react-native/src/validate.ts
Normal file
28
packages/react-native/src/validate.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
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, ""];
|
||||
}
|
16
packages/react-native/src/version.ts
Normal file
16
packages/react-native/src/version.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
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;
|
24
packages/react-native/tsconfig.json
Normal file
24
packages/react-native/tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"types": ["react-native"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/index.ts", "*.d.ts"]
|
||||
}
|
33
packages/react-native/vite.config.mjs
Normal file
33
packages/react-native/vite.config.mjs
Normal file
|
@ -0,0 +1,33 @@
|
|||
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,
|
||||
}),
|
||||
],
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue