Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/design/2026-09-15-sdk-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The single most important finding behind this design: **iOS 7.x and Android 7.x

## 2. Goals

- Ship a 7.x-native React Native SDK for the two current native lines: Android **7.3.0** (pinned transitionally as `7.3.0-SNAPSHOT` while the release is pending — see the plan's Phase 3 rulings) and iOS **7.0.0-beta5** (SPM only, iOS deployment target 15.0).
- Ship a 7.x-native React Native SDK for the two current native lines: Android **7.3.1** (7.3.0 first, pinned transitionally as `7.3.0-SNAPSHOT` while that release was pending — see the plan's Phase 3 rulings; 7.3.1 fixes bugsee-android #214–#221 found by the beta campaign) and iOS **7.0.0-beta5** (SPM only, iOS deployment target 15.0).
- Expose the capabilities 7.x added and 6.x never had: breadcrumbs, notification relay, APM, user identity, hang/HTTP-error/frustration/anomaly detection, SDK status, report handlers.
- Make the wrapper's option surface **provably** consistent with the native SDKs, enforced in CI rather than by review.
- Work on bare React Native and on Expo, including Expo apps that regenerate native projects with `prebuild`.
Expand Down Expand Up @@ -422,7 +422,7 @@ Four implementation constraints:
- iOS Debug builds skip the upload unless `BUGSEE_UPLOAD_DEBUG_SOURCEMAPS=true`.
- **`uploadSymbols`** — default on, both platforms.
- iOS: `false` removes the Archive dSYM post-action.
- Android: `false` disables the Gradle plugin's `uploadBugsee*` tasks (R8 mapping, NDK symbols, build info) in a marked block, because Gradle plugin 4.0.7 has no switch of its own.
- Android: `false` disables the Gradle plugin's `uploadBugsee*` tasks (R8 mapping, NDK symbols, build info) in a marked block, because the Gradle plugin (4.0.7, and still 4.0.8) has no switch of its own.
- **`nativeCrashReporting`** — default on (§11.1).
- **`gradlePluginVersion`** — default: the pin in `native-versions.json`. A `--no-clean` prebuild rewrites an older pin in place.
- **`autoLaunch`** — default off. Writes the manifest token for a real token only.
Expand Down
2 changes: 1 addition & 1 deletion docs/design/plans/2026-09-16-implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
Every phase's requirements implicitly include this section.

- React Native floor **0.81.0**, New Architecture required. No `oldarch` source set, no legacy bridge fallback. 0.81 still allows opting out of the New Architecture; that is a documented requirement on the consumer, not a second code path. Hard technical floor is 0.80 (`codegenConfig.ios.modulesProvider`).
- Android SDK **7.3.0** (pinned transitionally as `7.3.0-SNAPSHOT` while the release is pending — see Phase 3's rulings), Gradle plugin **4.0.7**, pinned exactly. Neither older pin carries what this wrapper needs: 7.2.0 has no wrapper channel, report-contract methods, `BugseeReportHandlerThread` or `com.bugsee.option.$$WRAPPER` consumer, and 4.0.6 strips every extension's provider (workbook 1.4). The plugin marker resolves from **Maven Central**, not the Gradle Plugin Portal.
- Android SDK **7.3.1** (7.3.0 first, pinned transitionally as `7.3.0-SNAPSHOT` while that release was pending — see Phase 3's rulings; 7.3.1 fixes bugsee-android #214–#221), Gradle plugin **4.0.8** (4.0.7 first; 4.0.8 uploads native symbols from the unstripped `merged_native_libs`, independent of `debugSymbolLevel`, with a bugsee-cli floor of 0.8.1), pinned exactly. Neither older pin carries what this wrapper needs: 7.2.0 has no wrapper channel, report-contract methods, `BugseeReportHandlerThread` or `com.bugsee.option.$$WRAPPER` consumer, and 4.0.6 strips every extension's provider (workbook 1.4). The plugin marker resolves from **Maven Central**, not the Gradle Plugin Portal.
- iOS SDK **7.0.0-beta5** from `https://github.com/bugsee/spm`, requirement `exact`. SwiftPM will not admit a prerelease into a range. iOS deployment target **15.0**.
- Package `@bugsee/react-native`. `toSwiftName` maps it to `ReactNative`, which is **reserved**, so `react-native.config.js` pins `spm: { name: 'BugseeReactNative' }` and the SPM product name must match exactly.
- All native versions live in **one** file, `native-versions.json`, consumed by the podspec, `Package.swift` and the Gradle module. The SPM pin and podspec URL must never disagree.
Expand Down
18 changes: 5 additions & 13 deletions examples/bare/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,10 @@ android {
buildTypes {
debug {
signingConfig signingConfigs.debug
// AGP defaults this to NONE, so the plugin's native upload finds
// nothing and skips. SYMBOL_TABLE emits symbols for code this app
// builds. Maven Hermes and libreactnative.so are pre-stripped;
// this level does not symbolicate those two.
ndk {
debugSymbolLevel 'SYMBOL_TABLE'
}
// No ndk.debugSymbolLevel: Bugsee Gradle plugin 4.0.8 uploads
// native symbols from the unstripped libraries in
// build/intermediates/merged_native_libs, whatever the level.
// The level only decides what AGP packs for Google Play.
}
release {
// Caution! In production, you need to generate your own keystore file.
Expand All @@ -135,12 +132,7 @@ android {
debuggable findProperty('bugseeE2eDebuggable') == 'true'
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
// Same level as debug: run-android.sh is assembleDebug, and a
// release build needs the metadata too. Pre-stripped Maven Hermes
// and libreactnative.so are not symbolicated by this.
ndk {
debugSymbolLevel 'SYMBOL_TABLE'
}
// No ndk.debugSymbolLevel here either (see debug).
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions examples/bare/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ plugins {
// 'com.android.library' first!", since it hangs its tasks off an Android
// variant. The token file still belongs to this directory, the root
// project, not to app/.
id 'com.bugsee.android.gradle' version '4.0.7' apply false
id 'com.bugsee.android.gradle' version '4.0.8' apply false
}

// The token lives in bugsee.properties next to this file — the *root* Gradle
Expand All @@ -40,7 +40,7 @@ plugins {
def nativeVersions = new groovy.json.JsonSlurper().parse(file("../../../native-versions.json"))

def pinned = nativeVersions.android.gradlePlugin
def applied = '4.0.7'
def applied = '4.0.8'
if (pinned != applied) {
throw new GradleException(
"Bugsee Gradle plugin is pinned to ${pinned} in native-versions.json " +
Expand Down
33 changes: 10 additions & 23 deletions examples/bare/e2e/apm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Marker, from scenarios/apm.ts:
* BUGSEE_E2E apm notify=notify-<n> txn=txn-<n>
*/
import { performanceTransactions } from '../../../scripts/performance-capture';
import { type PulledBundle, airplane, relayTexts, removePulledBundles, terminateIosApp } from './bundles';
import { ANDROID_PACKAGE } from './device';
import {
Expand Down Expand Up @@ -124,30 +125,16 @@ describeDevice(`a notification and a transaction on ${TARGET_NAME}`, () => {
});
});

/** The span object whose `description` is `wanted`, anywhere in the capture. */
/**
* The span whose `description` is `wanted`, in any transaction of the capture.
* performanceTransactions reads both layouts: iOS's legacy one and Android
* 7.3.1's OTLP (bugsee-android #207), where the description is
* `bugsee.description` and the attributes are flattened to key -> value.
*/
function spanByDescription(capture: string, wanted: string): { attributes?: unknown } | undefined {
let found: { attributes?: unknown } | undefined;
const walk = (node: unknown): void => {
if (found !== undefined || node === null || typeof node !== 'object') {
return;
}
if (Array.isArray(node)) {
for (const item of node) {
walk(item);
}
return;
}
const record = node as { description?: unknown; attributes?: unknown };
if (record.description === wanted) {
found = record;
return;
}
for (const value of Object.values(record)) {
walk(value);
}
};
walk(JSON.parse(capture) as unknown);
return found;
return performanceTransactions(capture)
.flatMap(transaction => transaction.spans ?? [])
.find(span => span.description === wanted);
}

async function awaitRelay(title: string, timeoutMs = 20_000): Promise<string[]> {
Expand Down
17 changes: 10 additions & 7 deletions examples/bare/e2e/exceptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
* BUGSEE_E2E exc boundary-onError / boundary-fallback
* BUGSEE_E2E exc app-handler fatal=<bool>
*
* Cases 7 and 10 are `it.failing` on Android for documented 7.3.0 gaps
* (labels ignored; unhandled also files an error report). Do not weaken them.
* Case 7 is `it.failing` on Android for a documented gap (labels ignored,
* still in 7.3.1). Do not weaken it. Case 10 was `it.failing` on 7.3.0, which
* also filed an error report for an unhandled exception; 7.3.1 files the
* crash only (bugsee-android d390b5203, #192), so it is a plain `it`.
* On iOS both are plain `it` (labels apply; Debug files no second report).
*
* Case 12's red-box line must stay on Android. Gated release R2/R3 (controller
Expand Down Expand Up @@ -489,8 +491,9 @@ describeDevice(`JS exceptions on ${TARGET_NAME}`, () => {
alive,
});

// 7.3.0 also files an error report for the same incident (case 10).
// Wait for at least the crash, then a short beat for the extra error.
// 7.3.0 also filed an error report for the same incident (case 10).
// Wait for at least the crash, then a short beat so an extra report,
// if one is filed, is seen by case 10.
await awaitBundles(1, 30_000);
await new Promise(resolve => setTimeout(resolve, 3_000));
bundles = await pullAndroidBundles();
Expand All @@ -514,7 +517,7 @@ describeDevice(`JS exceptions on ${TARGET_NAME}`, () => {
expect(alive).toBe(true);
});

itAndroid.failing(
itAndroid(
'the fatal error files no second report for the incident',
() => {
const others = bundles.filter(b => {
Expand Down Expand Up @@ -798,8 +801,8 @@ describeDevice(`JS exceptions on ${TARGET_NAME}`, () => {
report('R2 single bundle', { file: only.file, type: only.request.type });
});

// Not it.failing on release: debug case 10 stays it.failing for the
// crash+error double-file; release retains one bundle for the incident.
// Release retains one bundle for the incident (debug case 10 asserts the
// same since 7.3.1 stopped filing an error report next to the crash).
itAndroid('exactly one bundle contains the fatal reason', () => {
const withFatal = bundles.filter(b => rawCrashContains(b, `E2E fatal ${nonce}`));
expect(withFatal).toHaveLength(1);
Expand Down
63 changes: 30 additions & 33 deletions examples/bare/e2e/feedback-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ const MIN_PIXELS = 40;

/** Keys the open chat paints, per platform. */
const CHAT_PIXEL: Record<'android' | 'ios', readonly string[]> = {
android: ['actionBarColor', 'backgroundColor', 'inputTextColor', 'inputTextHintColor', 'titleTextColor'],
// inputTextColor: its own test below, on typed text (7.3.1 draws the hint in inputTextHintColor).
android: ['actionBarColor', 'backgroundColor', 'inputTextHintColor', 'titleTextColor'],
ios: ['backgroundColor', 'barsColor', 'closeButtonColor', 'inputBackgroundColor', 'navigationBarColor', 'titleTextColor'],
};
/** Keys the e-mail screen paints, per platform. */
Expand All @@ -58,26 +59,6 @@ const EMAIL_PIXEL: Record<'android' | 'ios', readonly string[]> = {
ios: ['emailBackgroundColor', 'emailContinueNotActiveColor', 'emailSkipColor'],
};

function blend(top: string, under: string, alpha: number): string {
const channel = (hex: string, at: number) => Number.parseInt(hex.slice(at, at + 2), 16);
return `#${[1, 3, 5]
.map(at => Math.round(alpha * channel(top, at) + (1 - alpha) * channel(under, at)).toString(16).padStart(2, '0'))
.join('')}`;
}

/**
* The colour a key shows as, where the SDK draws it blended. Android 7.3.0's
* Compose chat draws the empty input's hint in `inputText` at alpha 0.5 over
* the background (ChatMessageInput.kt), so that is where inputTextColor shows;
* `bottomDelimiterColor` is the e-mail field's border (RequestEmailScreen.kt).
*/
function paintedAs(key: string): string {
if (ON_ANDROID && key === 'inputTextColor') {
return blend(FEEDBACK_COLOURS.inputTextColor!, FEEDBACK_COLOURS.backgroundColor!, 0.5);
}
return FEEDBACK_COLOURS[key]!;
}

/**
* iOS 7.0.0-beta5's feedback (BugseeFeedback, SwiftUI) has no e-mail screen:
* showFeedbackUI() presents FeedbackChatView with Bugsee.getUserIdentifier()
Expand All @@ -89,18 +70,13 @@ const IOS_NO_EMAIL_SCREEN = 'iOS beta5 feedback has no e-mail screen (bugsee-coc
const IOS_NO_EMAIL_SCREEN_KEY = 'iOS beta5 feedback has no e-mail screen to paint (bugsee-cocoa#202)';

/**
* Keys Android 7.3.0 reads back but never paints, pinned (it.failing): the
* feedback module (FeedbackColors.kt, ChatMessageInput.kt) never reads
* Feedback::ActionBarColor or Feedback::EmailContinueNotActiveColor, and the
* chat's hint is drawn from InputTextColor, not InputTextHintColor
* (WOD_LX1, 2026-10-07). Filed: bugsee-android#218.
* Keys a platform reads back but never paints, pinned (it.failing). Android
* 7.3.0 never read Feedback::ActionBarColor or EmailContinueNotActiveColor
* and drew the hint from InputTextColor (bugsee-android#218); 7.3.1 paints
* all three (#226, WOD_LX1), so Android has none.
*/
const KNOWN_NOT_PAINTED: Record<'android' | 'ios', Record<string, string>> = {
android: {
actionBarColor: 'Android 7.3.0 never reads Feedback::ActionBarColor (bugsee-android#218)',
emailContinueNotActiveColor: 'Android 7.3.0 never reads Feedback::EmailContinueNotActiveColor (bugsee-android#218)',
inputTextHintColor: 'Android 7.3.0 draws the hint from InputTextColor at alpha 0.5 (bugsee-android#218)',
},
android: {},
ios: {
emailBackgroundColor: IOS_NO_EMAIL_SCREEN_KEY,
emailContinueNotActiveColor: IOS_NO_EMAIL_SCREEN_KEY,
Expand All @@ -112,8 +88,8 @@ const KNOWN_NOT_PAINTED: Record<'android' | 'ios', Record<string, string>> = {
function pixelIt(base: jest.It, key: string, where: string, shot: () => string): void {
const known = KNOWN_NOT_PAINTED[PLATFORM][key];
(known !== undefined ? base.failing : base)(`[${faOf(key)}] ${key} paints the ${where}${known !== undefined ? ` [known: ${known}]` : ''}`, async () => {
const found = await colourPixels(shot(), colourOf(paintedAs(key)), 20);
report(`${key} ${paintedAs(key)} pixels`, { count: found.count, box: found.box });
const found = await colourPixels(shot(), colourOf(FEEDBACK_COLOURS[key]!), 20);
report(`${key} ${FEEDBACK_COLOURS[key]} pixels`, { count: found.count, box: found.box });
expect(found.count).toBeGreaterThan(MIN_PIXELS);
});
}
Expand Down Expand Up @@ -224,6 +200,27 @@ describeDevice(`the feedback package's keys, nulls and pre-launch use on ${TARGE
}
pixelIt(it, key, 'chat', () => shot);
}

/**
* Android 7.3.0 drew the empty input's hint in inputTextColor at alpha 0.5,
* which is where this key used to be seen; 7.3.1 draws the hint in
* inputTextHintColor (bugsee-android #218 via #226). The input text colour
* is now only on text in the field, so this types some (nothing is sent).
*/
(ON_ANDROID ? it : it.skip)('[FA-19] inputTextColor paints text typed into the chat input', async () => {
const { xml } = await uiDump();
const field = /<node [^>]*class="android\.widget\.EditText"[^>]*>/.exec(xml)?.[0];
expect(field).toBeDefined();
const [left, top, right, bottom] = /bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(field!)!.slice(1, 5).map(Number) as [number, number, number, number];
await adbStatus('shell', 'input', 'tap', String(Math.round((left + right) / 2)), String(Math.round((top + bottom) / 2)));
await adbStatus('shell', 'input', 'text', 'WWWWWWWWWWWW');
await new Promise(resolve => setTimeout(resolve, 1_500));
const typed = await captureScreen('feedback-typed');
keepShot(typed, `chat-typed-${run.scenario.nonce}`);
const found = await colourPixels(typed, colourOf(FEEDBACK_COLOURS.inputTextColor!), 20);
report(`inputTextColor ${FEEDBACK_COLOURS.inputTextColor} typed pixels`, { count: found.count, box: found.box });
expect(found.count).toBeGreaterThan(MIN_PIXELS);
});
});

describe('the e-mail screen on a first open', () => {
Expand Down
15 changes: 10 additions & 5 deletions examples/bare/e2e/feedback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ const CHAT_BACKGROUND = '#112233';
*/
const itColour = ON_IOS ? it.failing : it;
/**
* Android 7.3.0 stores the greeting (`setDefaultFeedbackGreeting` writes the
* feedback preferences' `greeting`) and nothing ever reads it back: the chat
* opens empty, offline. iOS shows it. Pinned until the Android SDK renders it.
* Filed: https://github.com/bugsee/bugsee-android/issues/215
* Android 7.3.0 stored the greeting and never read it back: the chat opened
* empty (bugsee-android #215). 7.3.1 (#225) is meant to show it in an empty
* chat, as iOS does, but on this flow (greeting set just before
* showFeedbackActivity, chat opened directly, offline) the WOD_LX1 still shows
* an empty chat while the feedback preferences hold the greeting, 2 of 2 runs:
* https://github.com/bugsee/bugsee-android/issues/245. Pinned until it shows.
*/
const itGreeting = ON_ANDROID ? it.failing : it;

Expand Down Expand Up @@ -103,8 +105,11 @@ describeDevice(`the feedback chat on ${TARGET_NAME}`, () => {
expect(top).toBe(FEEDBACK_ACTIVITY);
}
// Both platforms title the screen "Feedback", in every locale tried.
// Since Android 7.3.1 paints the feedback action bar (bugsee-android
// #218), OCR can read the back arrow into the title's line ("< Feedback");
// the arrow glyph is dropped, the title still has to match exactly.
const text = await screenText('feedback');
expect(text).toContain('Feedback');
expect(text.map(line => line.replace(/^[<‹←]\s*/, ''))).toContain('Feedback');
// The app's own screen is gone from view: its status label is not read.
expect(text).not.toContain('Bugsee React Native');
});
Expand Down
Loading
Loading