Skip to content
Merged
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: 4 additions & 0 deletions docs/design/plans/2026-09-16-implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -4057,6 +4057,10 @@ Review fix: `finish` drops the handle it was called with even when `isFinished`

`appearance` over two mapping tables — Android is keyed (`setColor(ReportAppearance.X, int)`), iOS is property-based on `BugseeTheme`. `deleteCollectedDataOnDevice(includingIntermediate)`. Feedback appearance is **not** here; it ships with the feedback package, since `FeedbackAppearance` lives in the feedback artifact on Android.

The tables are the 7.x report colors: Android `Report::…` constants from bugsee-android 7.3.0 (`ReportAppearance`), iOS writable `report*Color` properties on `BugseeTheme` in the vendored 7.0.0-beta3 header. The 6.x Android names (`ReportBackgroundColor`, no `Report::` prefix) are not used. String placeholders stay off this surface (`setString`, not `setColor`).

Device: WOD_LX1 `AMRJCP4718402860`, Debug, logged `BUGSEE_E2E appearance nonce=97aed4863fa7 background=#ff0000ff includingIntermediate=true deleted=false` (pass: the SDK read `#ff0000ff` back). iOS Simulator `6FA9B3E8-26C7-4232-AA2C-537D9DF32957`, Debug, logged `BUGSEE_E2E appearance nonce=603f8a16a924 background=#ff0000ff includingIntermediate=true deleted=false` (pass, same read-back; SDK `7.0.0-beta3` `0d9c9d0a-9`). `deleted=false` is the SDK refusing the deletion while launched. iPhone XS (KRSFT) was already running BareExample (pid 43437); waited and retried once, and that process was still there, so that run was not taken.

---

## Phase 12 — Feedback package
Expand Down
6 changes: 6 additions & 0 deletions examples/bare/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import {
} from './scenarios/add-network-event';
import { isFeedbackScenario, runFeedbackScenario } from './scenarios/feedback';
import { isApmScenario, runApmScenario } from './scenarios/apm';
import { isAppearanceScenario, runAppearanceScenario } from './scenarios/appearance';

const STATUS_NAMES: Record<number, string> = {
[Status.Stopped]: 'Stopped',
Expand Down Expand Up @@ -499,6 +500,11 @@ export default function App() {
return;
}

if (isAppearanceScenario(choice.scenario)) {
await runAppearanceScenario(choice.scenario, choice.nonce);
return;
}

if (isExceptionScenario(choice.scenario)) {
if (isExceptionRenderScenario(choice.scenario)) {
if (!cancelled) {
Expand Down
95 changes: 95 additions & 0 deletions examples/bare/e2e/appearance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Phase 11: appearance is written and read back, then collected data is
* deleted, after launch. A second launch stops the SDK and deletes again.
*
* Markers, from scenarios/appearance.ts:
* BUGSEE_E2E appearance nonce=<n> background=#ff0000ff includingIntermediate=true deleted=false
* BUGSEE_E2E appearance nonce=<n> stopped=true includingIntermediate=true deleted=true
*
* The color in the launched line is what the SDK returned. While launched the
* SDK refuses deletion. After stop it must succeed. `duration` stays 90.
*/
import { terminateIosApp } from './bundles';
import { ANDROID_PACKAGE } from './device';
import {
ON_IOS,
type Run,
TARGET_NAME,
describeDevice,
must,
report,
startDeviceLog,
startRun,
stopDeviceLog,
} from './harness';
import { type DeviceLog, adb, resetScenario } from './scenario';

jest.setTimeout(5 * 60_000);

describeDevice(`appearance and data deletion on ${TARGET_NAME}`, () => {
let log: DeviceLog | undefined;

beforeAll(async () => {
log = await startDeviceLog('11', '11');
});

afterAll(async () => {
try {
if (ON_IOS) {
await terminateIosApp();
} else {
await adb('shell', 'am', 'force-stop', ANDROID_PACKAGE).catch(() => {});
}
} finally {
stopDeviceLog(log);
resetScenario();
}
});

async function launch(name: string): Promise<Run> {
const run = await startRun(name);
report('banner', run.banner.text.trim());
report('build', run.dev ? 'Debug' : 'Release');
return run;
}

it('reads the stored color back and records that deletion was refused', async () => {
const run = await launch('appearance');
const nonce = run.scenario.nonce;
const found = must(
await log!.waitFor(
new RegExp(
`BUGSEE_E2E appearance nonce=${nonce} background=#ff0000ff includingIntermediate=true deleted=false`,
),
20_000,
run.launched.index,
),
'the launched appearance line with deleted=false',
run.start,
);
expect(found.text).toContain(`nonce=${nonce}`);
expect(found.text).toContain('background=#ff0000ff');
expect(found.text).toContain('includingIntermediate=true');
expect(found.text).toContain('deleted=false');
});

it('deletes collected data after stop', async () => {
const run = await launch('appearance-stopped');
const nonce = run.scenario.nonce;
const found = must(
await log!.waitFor(
new RegExp(
`BUGSEE_E2E appearance nonce=${nonce} stopped=true includingIntermediate=true deleted=`,
),
20_000,
run.launched.index,
),
'the stop-then-delete appearance line',
run.start,
);
expect(found.text).toContain(`nonce=${nonce}`);
expect(found.text).toContain('stopped=true');
expect(found.text).toContain('includingIntermediate=true');
expect(found.text).toContain('deleted=true');
});
});
40 changes: 40 additions & 0 deletions examples/bare/scenarios/appearance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Phase 11: a report color and a collected-data deletion, after launch.
*
* `backgroundColor` exists on both platforms. The getter reads the SDK
* back, so the logged color is what was stored, not the string we wrote.
* `includingIntermediate` is the flag handed to
* `deleteCollectedDataOnDevice`. While launched the SDK refuses the
* deletion; the line still records the flag that crossed.
*/
import Bugsee from '@bugsee/react-native';

export const APPEARANCE_SCENARIOS = ['appearance', 'appearance-stopped'] as const;

export type AppearanceScenario = (typeof APPEARANCE_SCENARIOS)[number];

export function isAppearanceScenario(name: string): name is AppearanceScenario {
return (APPEARANCE_SCENARIOS as readonly string[]).includes(name);
}

const COLOR = '#ff0000ff';

export async function runAppearanceScenario(
scenario: AppearanceScenario,
nonce: string,
): Promise<void> {
if (scenario === 'appearance-stopped') {
await Bugsee.stop();
const deleted = await Bugsee.deleteCollectedDataOnDevice(true);
console.log(
`BUGSEE_E2E appearance nonce=${nonce} stopped=true includingIntermediate=true deleted=${String(deleted)}`,
);
return;
}
Bugsee.appearance.backgroundColor = COLOR;
const read = Bugsee.appearance.backgroundColor ?? 'unread';
const deleted = await Bugsee.deleteCollectedDataOnDevice(true);
console.log(
`BUGSEE_E2E appearance nonce=${nonce} background=${read} includingIntermediate=true deleted=${String(deleted)}`,
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.bugsee.reactnative;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.bugsee.library.Bugsee;
import com.bugsee.library.contracts.appearance.Appearance;

import java.util.Locale;

/**
* Report colors through {@code Bugsee.getAppearance()}.
*
* <p>{@code property} is a {@code ReportAppearance} constant value, such as
* {@code ReportAppearance.BackgroundColor} ({@code Report::BackgroundColor}).
* Components are 0–255. Android packs them ARGB, alpha in the high byte.
*/
public final class AppearanceBridge {

private AppearanceBridge() {
}

public static void setColor(
@NonNull final String property,
final int r,
final int g,
final int b,
final int a
) {
if (property.length() == 0) {
throw new IllegalArgumentException("report appearance property is empty");
}
checkComponent(r);
checkComponent(g);
checkComponent(b);
checkComponent(a);
final Appearance appearance = Bugsee.getAppearance();
appearance.setColor(property, Integer.valueOf(packArgb(r, g, b, a)));
}

@Nullable
@ColorInt
public static Integer getColor(@NonNull final String property) {
if (property.length() == 0) {
return null;
}
return Bugsee.getAppearance().getColor(property);
}

/**
* {@code #rrggbbaa}, or {@code ""} when the SDK has no color stored.
* Alpha is last, matching the CSS hex the JS setter accepts.
*/
@NonNull
public static String toHex(@Nullable final Integer color) {
if (color == null) {
return "";
}
final int packed = color.intValue();
return String.format(
Locale.US,
"#%02x%02x%02x%02x",
Integer.valueOf((packed >> 16) & 0xff),
Integer.valueOf((packed >> 8) & 0xff),
Integer.valueOf(packed & 0xff),
Integer.valueOf((packed >>> 24) & 0xff)
);
}

static int packArgb(final int r, final int g, final int b, final int a) {
return (a & 0xff) << 24 | (r & 0xff) << 16 | (g & 0xff) << 8 | (b & 0xff);
}

private static void checkComponent(final int value) {
if (value < 0 || value > 255) {
throw new IllegalArgumentException("color component out of range: " + value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,45 @@ private static Map<String, Object> attributes(
}
}

// --- Appearance and collected data (Phase 11) ---------------------------
// set/get return a value so codegen runs them on the JS thread, ahead of
// the read that follows a write in the same turn.

@Override
public boolean setAppearanceColor(
final String name,
final double r,
final double g,
final double b,
final double a
) {
AppearanceBridge.setColor(
name,
(int) Math.round(r),
(int) Math.round(g),
(int) Math.round(b),
(int) Math.round(a)
);
return true;
}

@Override
@NonNull
public String getAppearanceColor(final String name) {
return AppearanceBridge.toHex(AppearanceBridge.getColor(name));
}

@Override
public void deleteCollectedDataOnDevice(
final boolean includingIntermediate,
final Promise promise
) {
Bugsee.deleteCollectedDataOnDevice(
includingIntermediate,
success -> promise.resolve(Boolean.TRUE.equals(success))
);
}

/** One JSON value, or null when it is missing, null, an object or an array. */
@Nullable
private static Object jsonValue(@Nullable final String json) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.bugsee.reactnative;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import com.bugsee.library.Bugsee;
import com.bugsee.library.contracts.appearance.ReportAppearance;

import org.junit.Test;

public class AppearanceBridgeTest {

@Test
public void setColorStoresTheReportAppearanceConstant() {
final int packed = AppearanceBridge.packArgb(0x11, 0x22, 0x33, 0x44);
AppearanceBridge.setColor(ReportAppearance.BackgroundColor, 0x11, 0x22, 0x33, 0x44);
assertEquals(
Integer.valueOf(packed),
Bugsee.getAppearance().getColor(ReportAppearance.BackgroundColor));
assertEquals("Report::BackgroundColor", ReportAppearance.BackgroundColor);
assertEquals("#11223344", AppearanceBridge.getColor(ReportAppearance.BackgroundColor) == null
? ""
: AppearanceBridge.toHex(AppearanceBridge.getColor(ReportAppearance.BackgroundColor)));
}

@Test
public void eachReportColorConstantRoundTrips() {
final String[] keys = new String[] {
ReportAppearance.ActionBarColor,
ReportAppearance.ActionBarTextColor,
ReportAppearance.ActionBarButtonBackgroundClickedColor,
ReportAppearance.BackgroundColor,
ReportAppearance.EditTextBackgroundColor,
ReportAppearance.HintColor,
ReportAppearance.SeverityLabelActiveColor,
ReportAppearance.TextColor,
ReportAppearance.VersionColor,
};
int n = 1;
for (final String key : keys) {
AppearanceBridge.setColor(key, n, n + 1, n + 2, 255);
assertEquals(
Integer.valueOf(AppearanceBridge.packArgb(n, n + 1, n + 2, 255)),
AppearanceBridge.getColor(key));
assertTrue(key.startsWith("Report::"));
n += 3;
}
}

@Test
public void packArgbUsesAndroidChannelOrder() {
assertEquals(0xFF000000, AppearanceBridge.packArgb(0, 0, 0, 255));
assertEquals(0xFFFF0000, AppearanceBridge.packArgb(255, 0, 0, 255));
assertEquals(0x44223311, AppearanceBridge.packArgb(0x22, 0x33, 0x11, 0x44));
}

@Test
public void toHexPutsAlphaLast() {
assertEquals("#11223344", AppearanceBridge.toHex(Integer.valueOf(0x44112233)));
assertEquals("", AppearanceBridge.toHex(null));
}

@Test
public void anOutOfRangeComponentDoesNotReplaceTheStoredColor() {
AppearanceBridge.setColor(ReportAppearance.TextColor, 1, 2, 3, 4);
final Integer before = Bugsee.getAppearance().getColor(ReportAppearance.TextColor);
try {
AppearanceBridge.setColor(ReportAppearance.TextColor, 256, 0, 0, 255);
throw new AssertionError("expected IllegalArgumentException");
} catch (final IllegalArgumentException expected) {
assertTrue(expected.getMessage().contains("256"));
}
assertEquals(before, Bugsee.getAppearance().getColor(ReportAppearance.TextColor));
}

@Test
public void anEmptyPropertyIsRejected() {
try {
AppearanceBridge.setColor("", 0, 0, 0, 255);
throw new AssertionError("expected IllegalArgumentException");
} catch (final IllegalArgumentException expected) {
assertTrue(expected.getMessage().contains("empty"));
}
assertNull(AppearanceBridge.getColor(""));
}
}
Loading
Loading