@sansavision/apphelm-react-native-profiler 0.0.0-stage → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ # React Native's version decides which Hermes API the module calls (IHermesRootAPI from 0.81, as
6
+ # Sentry React Native's podspec decides) and whether Hermes is the default engine (0.70 and later).
7
+ rn_package_path = ENV["REACT_NATIVE_NODE_MODULES_DIR"] ? File.join(ENV["REACT_NATIVE_NODE_MODULES_DIR"], "package.json") : `node --print "require.resolve('react-native/package.json')"`.strip
8
+ rn_version = File.exist?(rn_package_path) ? JSON.parse(File.read(rn_package_path))["version"].to_s.split(".").map(&:to_i) : [0, 0]
9
+ rn_major, rn_minor = rn_version[0] || 0, rn_version[1] || 0
10
+ new_hermes_runtime = rn_major >= 1 || (rn_major == 0 && rn_minor >= 81)
11
+ hermes_default = rn_major >= 1 || (rn_major == 0 && rn_minor >= 70)
12
+ using_hermes = (ENV["USE_HERMES"].nil? && hermes_default) || ENV["USE_HERMES"] == "1"
13
+
14
+ Pod::Spec.new do |s|
15
+ s.name = "ApphelmReactNativeProfiler"
16
+ s.version = package["version"]
17
+ s.summary = package["description"]
18
+ s.homepage = "https://apphelm.dev"
19
+ s.license = { :type => "Proprietary", :file => "LICENSE" }
20
+ s.authors = "SansaVision"
21
+ s.platforms = { :ios => "13.4" }
22
+ s.source = { :git => "https://github.com/Sansa-Organisation/apphelm.git", :tag => "v#{s.version}" }
23
+ s.source_files = "ios/**/*.{h,m,mm}"
24
+ s.requires_arc = true
25
+ s.pod_target_xcconfig = {
26
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
27
+ "OTHER_CPLUSPLUSFLAGS" => "$(inherited)" + (new_hermes_runtime ? " -DAPPHELM_NEW_HERMES_RUNTIME" : ""),
28
+ }
29
+
30
+ s.dependency "React-Core"
31
+ if using_hermes
32
+ s.dependency "React-hermes"
33
+ s.dependency "hermes-engine"
34
+ end
35
+ end
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.8.0 (prepared 7 Nov 2026, not published)
4
+
5
+ - First version (for SDK 0.8.0): the native half of `@sansavision/apphelm/react-native-profiling`. An
6
+ Android module (`HermesSamplingProfiler.enable()`, `disable()`, `dumpSampledTraceToFile()` into the
7
+ cache directory) and an iOS module (`HermesRuntime::enableSamplingProfiler()` and
8
+ `dumpSampledTraceToStream()`, or `IHermesRootAPI` from React Native 0.81), as Sentry React Native
9
+ starts Hermes's sampling profiler. `hermesSampler` is null when the module is not linked. Compiled
10
+ against stubs of React Native's classes and headers; not yet built in an app or run on a device.
package/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ Copyright 2026 SansaVision. All rights reserved.
2
+
3
+ This package may be used only under an active Apphelm service agreement. Redistribution, sublicensing, and reverse engineering are prohibited except where applicable law does not permit those restrictions.
package/README.md CHANGED
@@ -1,3 +1,42 @@
1
- # Temporary Holding Version
1
+ # @sansavision/apphelm-react-native-profiler
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The native half of AppHelm's React Native profiling. It starts and stops Hermes's sampling profiler on Android and iOS, so `@sansavision/apphelm/react-native-profiling` can profile your app's sampled transactions. The rest of the AppHelm React Native SDK has no native code: install this package only if you want profiles.
4
+
5
+ ```sh
6
+ npm install @sansavision/apphelm @sansavision/apphelm-react-native-profiler
7
+ cd ios && pod install
8
+ ```
9
+
10
+ React Native's autolinking picks the module up (Android: `ApphelmHermesProfilerPackage`; iOS: the `ApphelmReactNativeProfiler` pod). Rebuild the app afterwards: a JavaScript reload is not enough.
11
+
12
+ ```ts
13
+ import { Platform } from "react-native";
14
+ import { createReactNativeSdk } from "@sansavision/apphelm/react-native";
15
+ import { hermesProfiling } from "@sansavision/apphelm/react-native-profiling";
16
+ import { hermesSampler } from "@sansavision/apphelm-react-native-profiler";
17
+
18
+ const apphelm = createReactNativeSdk({
19
+ apiKey, workspaceId, productId, release: "app@2.3.0+45",
20
+ tracesSampleRate: 0.2,
21
+ profiling: hermesProfiling({ profilesSampleRate: 0.5, sampler: hermesSampler, platform: Platform.OS }),
22
+ });
23
+ ```
24
+
25
+ ## What it does
26
+
27
+ - `start()` calls `HermesSamplingProfiler.enable()` (Android) or `HermesRuntime::enableSamplingProfiler()` (iOS; `IHermesRootAPI` from React Native 0.81). Hermes then samples the JavaScript thread about every 10 ms.
28
+ - `stop()` disables the profiler and returns its trace: on Android it is dumped to a file in the app's cache directory, read and deleted; on iOS it is written to memory.
29
+ - When the React instance goes away (a reload, the app closing) a running profiler is disabled.
30
+ - Every failure resolves to `false` or `null`, never an error: the transaction is then sent without a profile.
31
+
32
+ This is how Sentry React Native profiles Hermes (`RNSentryModuleImpl.startProfiling` on Android, `RNSentryHermesSampler.mm` on iOS). Unlike Sentry, AppHelm doesn't add a native platform profile (Android's method trace, or sentry-cocoa's) to the Hermes profile: the AppHelm Swift and Kotlin SDKs profile native code on their own.
33
+
34
+ ## Limits
35
+
36
+ - **Hermes only.** On JavaScriptCore `start()` resolves to `false` and nothing is profiled. Expo Go has no custom native modules: use a development build.
37
+ - `hermesSampler` is `null` when the module isn't linked (the app wasn't rebuilt); `hermesProfiling` then does nothing.
38
+ - One profile at a time, at most 30 seconds (the JavaScript side stops it).
39
+ - Release builds name frames by their Hermes bytecode position (`app:///index.android.bundle:1:<offset>`), with minified function names. AppHelm stores those positions; mapping them back to your source with uploaded Hermes source maps is not yet done for profiles (it is for errors).
40
+ - Not yet built in a React Native app or run on a device by AppHelm: the Java module was compiled against stubs of React Native's classes and the iOS module type-checked against stubs of its headers.
41
+
42
+ Requires React Native 0.71 or later.
@@ -0,0 +1,30 @@
1
+ // The Android half of @sansavision/apphelm-react-native-profiler, built by the app's Gradle
2
+ // through React Native's autolinking. HermesSamplingProfiler comes with React Native itself.
3
+ apply plugin: "com.android.library"
4
+
5
+ def safeExtGet(prop, fallback) {
6
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
7
+ }
8
+
9
+ android {
10
+ namespace "dev.apphelm.reactnative.profiler"
11
+ compileSdkVersion safeExtGet("compileSdkVersion", 35)
12
+ defaultConfig {
13
+ minSdkVersion safeExtGet("minSdkVersion", 24)
14
+ targetSdkVersion safeExtGet("targetSdkVersion", 35)
15
+ }
16
+ compileOptions {
17
+ sourceCompatibility JavaVersion.VERSION_17
18
+ targetCompatibility JavaVersion.VERSION_17
19
+ }
20
+ }
21
+
22
+ repositories {
23
+ google()
24
+ mavenCentral()
25
+ }
26
+
27
+ dependencies {
28
+ // Resolved by the React Native Gradle plugin to the app's react-android.
29
+ implementation "com.facebook.react:react-native:+"
30
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,106 @@
1
+ package dev.apphelm.reactnative.profiler;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.facebook.hermes.instrumentation.HermesSamplingProfiler;
5
+ import com.facebook.react.bridge.Promise;
6
+ import com.facebook.react.bridge.ReactApplicationContext;
7
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
8
+ import com.facebook.react.bridge.ReactMethod;
9
+ import java.io.ByteArrayOutputStream;
10
+ import java.io.File;
11
+ import java.io.FileInputStream;
12
+ import java.io.InputStream;
13
+ import java.nio.charset.StandardCharsets;
14
+
15
+ /**
16
+ * Starts and stops Hermes's sampling profiler for AppHelm's React Native profiling, as Sentry
17
+ * React Native's RNSentryModuleImpl does: {@code HermesSamplingProfiler.enable()}, then
18
+ * {@code disable()} and {@code dumpSampledTraceToFile()} into the app's cache directory; the file
19
+ * is read, returned to JavaScript and deleted. Every failure (JavaScriptCore, an old Hermes, no
20
+ * cache directory) resolves to false or null: profiling must never break the app.
21
+ */
22
+ public class ApphelmHermesProfilerModule extends ReactContextBaseJavaModule {
23
+ public static final String NAME = "ApphelmHermesProfiler";
24
+ /** A trace larger than this is not read (a 30-second profile is far smaller). */
25
+ private static final long MAX_TRACE_BYTES = 16L * 1024 * 1024;
26
+
27
+ private final Object lock = new Object();
28
+ private boolean running = false;
29
+
30
+ public ApphelmHermesProfilerModule(ReactApplicationContext context) {
31
+ super(context);
32
+ }
33
+
34
+ @NonNull
35
+ @Override
36
+ public String getName() {
37
+ return NAME;
38
+ }
39
+
40
+ @ReactMethod
41
+ public void start(Promise promise) {
42
+ synchronized (lock) {
43
+ try {
44
+ // Hermes keeps no state to ask about: clear a profile a previous run left enabled.
45
+ HermesSamplingProfiler.disable();
46
+ HermesSamplingProfiler.enable();
47
+ running = true;
48
+ promise.resolve(true);
49
+ } catch (Throwable error) {
50
+ running = false;
51
+ promise.resolve(false);
52
+ }
53
+ }
54
+ }
55
+
56
+ @ReactMethod
57
+ public void stop(Promise promise) {
58
+ File output = null;
59
+ try {
60
+ synchronized (lock) {
61
+ if (!running) {
62
+ promise.resolve(null);
63
+ return;
64
+ }
65
+ running = false;
66
+ HermesSamplingProfiler.disable();
67
+ }
68
+ output = File.createTempFile("apphelm-hermes-", ".cpuprofile", getReactApplicationContext().getCacheDir());
69
+ HermesSamplingProfiler.dumpSampledTraceToFile(output.getPath());
70
+ promise.resolve(read(output));
71
+ } catch (Throwable error) {
72
+ promise.resolve(null);
73
+ } finally {
74
+ if (output != null) {
75
+ //noinspection ResultOfMethodCallIgnored
76
+ output.delete();
77
+ }
78
+ }
79
+ }
80
+
81
+ /** The sampler must not outlive the React instance (a reload, the app closing). */
82
+ @Override
83
+ public void invalidate() {
84
+ synchronized (lock) {
85
+ if (running) {
86
+ running = false;
87
+ try {
88
+ HermesSamplingProfiler.disable();
89
+ } catch (Throwable ignored) {
90
+ // Best effort on teardown.
91
+ }
92
+ }
93
+ }
94
+ super.invalidate();
95
+ }
96
+
97
+ private static String read(File file) throws Exception {
98
+ if (!file.exists() || file.length() == 0 || file.length() > MAX_TRACE_BYTES) return null;
99
+ try (InputStream input = new FileInputStream(file); ByteArrayOutputStream bytes = new ByteArrayOutputStream((int) file.length())) {
100
+ byte[] buffer = new byte[64 * 1024];
101
+ int read;
102
+ while ((read = input.read(buffer)) != -1) bytes.write(buffer, 0, read);
103
+ return new String(bytes.toByteArray(), StandardCharsets.UTF_8);
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,24 @@
1
+ package dev.apphelm.reactnative.profiler;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.facebook.react.ReactPackage;
5
+ import com.facebook.react.bridge.NativeModule;
6
+ import com.facebook.react.bridge.ReactApplicationContext;
7
+ import com.facebook.react.uimanager.ViewManager;
8
+ import java.util.Collections;
9
+ import java.util.List;
10
+
11
+ /** Registered by React Native's autolinking. */
12
+ public class ApphelmHermesProfilerPackage implements ReactPackage {
13
+ @NonNull
14
+ @Override
15
+ public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext context) {
16
+ return Collections.<NativeModule>singletonList(new ApphelmHermesProfilerModule(context));
17
+ }
18
+
19
+ @NonNull
20
+ @Override
21
+ public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext context) {
22
+ return Collections.emptyList();
23
+ }
24
+ }
package/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** Hermes's sampling profiler, for `hermesProfiling({ sampler })`; null when the native module isn't linked. */
2
+ export declare const hermesSampler: {
3
+ /** Start Hermes's sampling profiler; false on JavaScriptCore or when it can't start. */
4
+ start(): Promise<boolean>;
5
+ /** Stop it and return its trace (Hermes's Chrome trace JSON), or null. */
6
+ stop(): Promise<string | null>;
7
+ } | null;
package/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // The native module (android/ and ios/) as the `sampler` that
2
+ // @sansavision/apphelm/react-native-profiling's `hermesProfiling` takes. When the module isn't
3
+ // linked (Expo Go, a build made before installing this package) it is null and nothing is profiled.
4
+ import { NativeModules } from "react-native";
5
+
6
+ const native = NativeModules.ApphelmHermesProfiler;
7
+
8
+ export const hermesSampler = native && typeof native.start === "function" && typeof native.stop === "function"
9
+ ? {
10
+ start: () => native.start().then((started) => started === true, () => false),
11
+ stop: () => native.stop().then((trace) => (typeof trace === "string" && trace.length > 0 ? trace : null), () => null),
12
+ }
13
+ : null;
@@ -0,0 +1,127 @@
1
+ // The iOS half of @sansavision/apphelm-react-native-profiler: starts and stops Hermes's sampling
2
+ // profiler for AppHelm's React Native profiling, as Sentry React Native's RNSentryHermesSampler.mm
3
+ // does (HermesRuntime::enableSamplingProfiler / disableSamplingProfiler /
4
+ // dumpSampledTraceToStream; the IHermesRootAPI form from React Native 0.81). Without Hermes
5
+ // (JavaScriptCore) `start` resolves to NO and nothing is profiled. No call ever rejects.
6
+
7
+ #import <Foundation/Foundation.h>
8
+ #import <React/RCTBridgeModule.h>
9
+ #import <mutex>
10
+ #import <sstream>
11
+ #import <string>
12
+
13
+ #if __has_include(<hermes/hermes.h>)
14
+ # define APPHELM_HERMES_SAMPLER 1
15
+ # import <hermes/hermes.h>
16
+ #else
17
+ # define APPHELM_HERMES_SAMPLER 0
18
+ #endif
19
+
20
+ namespace {
21
+ #if APPHELM_HERMES_SAMPLER
22
+ std::mutex samplerMutex;
23
+ bool running = false;
24
+
25
+ void enableSampler()
26
+ {
27
+ # ifdef APPHELM_NEW_HERMES_RUNTIME
28
+ auto *api = facebook::jsi::castInterface<facebook::hermes::IHermesRootAPI>(facebook::hermes::makeHermesRootAPI());
29
+ if (api) api->enableSamplingProfiler();
30
+ # else
31
+ facebook::hermes::HermesRuntime::enableSamplingProfiler();
32
+ # endif
33
+ }
34
+
35
+ void disableSampler()
36
+ {
37
+ # ifdef APPHELM_NEW_HERMES_RUNTIME
38
+ auto *api = facebook::jsi::castInterface<facebook::hermes::IHermesRootAPI>(facebook::hermes::makeHermesRootAPI());
39
+ if (api) api->disableSamplingProfiler();
40
+ # else
41
+ facebook::hermes::HermesRuntime::disableSamplingProfiler();
42
+ # endif
43
+ }
44
+
45
+ std::string dumpTrace()
46
+ {
47
+ std::stringstream stream;
48
+ # ifdef APPHELM_NEW_HERMES_RUNTIME
49
+ auto *api = facebook::jsi::castInterface<facebook::hermes::IHermesRootAPI>(facebook::hermes::makeHermesRootAPI());
50
+ if (api) api->dumpSampledTraceToStream(stream);
51
+ # else
52
+ facebook::hermes::HermesRuntime::dumpSampledTraceToStream(stream);
53
+ # endif
54
+ return stream.str();
55
+ }
56
+ #endif
57
+ } // namespace
58
+
59
+ @interface ApphelmHermesProfiler : NSObject <RCTBridgeModule>
60
+ @end
61
+
62
+ @implementation ApphelmHermesProfiler
63
+
64
+ RCT_EXPORT_MODULE(ApphelmHermesProfiler)
65
+
66
+ + (BOOL)requiresMainQueueSetup
67
+ {
68
+ return NO;
69
+ }
70
+
71
+ RCT_EXPORT_METHOD(start : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject)
72
+ {
73
+ #if APPHELM_HERMES_SAMPLER
74
+ std::lock_guard<std::mutex> lock(samplerMutex);
75
+ try {
76
+ // Hermes keeps no state to ask about: clear a profile a previous run left enabled.
77
+ disableSampler();
78
+ enableSampler();
79
+ running = true;
80
+ resolve(@YES);
81
+ } catch (...) {
82
+ running = false;
83
+ resolve(@NO);
84
+ }
85
+ #else
86
+ resolve(@NO);
87
+ #endif
88
+ }
89
+
90
+ RCT_EXPORT_METHOD(stop : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject)
91
+ {
92
+ #if APPHELM_HERMES_SAMPLER
93
+ std::lock_guard<std::mutex> lock(samplerMutex);
94
+ if (!running) {
95
+ resolve([NSNull null]);
96
+ return;
97
+ }
98
+ running = false;
99
+ try {
100
+ disableSampler();
101
+ std::string trace = dumpTrace();
102
+ NSString *text = trace.empty() ? nil : [[NSString alloc] initWithBytes:trace.data() length:trace.size() encoding:NSUTF8StringEncoding];
103
+ resolve(text ?: (id)[NSNull null]);
104
+ } catch (...) {
105
+ resolve([NSNull null]);
106
+ }
107
+ #else
108
+ resolve([NSNull null]);
109
+ #endif
110
+ }
111
+
112
+ // The sampler must not outlive the React instance (a reload, the app closing).
113
+ - (void)invalidate
114
+ {
115
+ #if APPHELM_HERMES_SAMPLER
116
+ std::lock_guard<std::mutex> lock(samplerMutex);
117
+ if (running) {
118
+ running = false;
119
+ try {
120
+ disableSampler();
121
+ } catch (...) {
122
+ }
123
+ }
124
+ #endif
125
+ }
126
+
127
+ @end
package/package.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "name": "@sansavision/apphelm-react-native-profiler",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.8.0",
4
+ "description": "The native half of AppHelm's React Native profiling: starts and stops Hermes's sampling profiler (Android and iOS) for @sansavision/apphelm/react-native-profiling.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./index.js",
8
+ "types": "./index.d.ts",
9
+ "exports": {
10
+ ".": { "types": "./index.d.ts", "import": "./index.js" }
11
+ },
12
+ "files": ["index.js", "index.d.ts", "ApphelmReactNativeProfiler.podspec", "ios", "android/build.gradle", "android/src", "README.md", "CHANGELOG.md", "LICENSE"],
13
+ "peerDependencies": { "react-native": ">=0.71" },
14
+ "peerDependenciesMeta": { "react-native": { "optional": true } },
15
+ "publishConfig": { "access": "public", "provenance": false }
16
+ }