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: 0 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,3 @@ T3CODE_RELAY_URL=https://relay.t3.codes
# Defaults to https://app.t3.codes; override to test against a staging deployment.
# T3CODE_HOSTED_APP_URL=https://nightly.app.t3.codes

# Public, ingest-only mobile OpenTelemetry configuration.
# T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces
# T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev
# T3CODE_MOBILE_OTLP_TRACES_TOKEN=xaat-...
10 changes: 4 additions & 6 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ const iosTeamIdOverride = (
)?.trim();
const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;

// Compiled into the native project. Without a relay, Observe's native side could dispatch to
// Expo's own ingest before observe.ts turns it off; `.invalid` never resolves (RFC 6761).
const observeEndpointUrl = repoEnv.T3CODE_RELAY_URL?.trim()
? new URL("/v1/mobile/observe", repoEnv.T3CODE_RELAY_URL.trim()).toString()
: "https://observe.invalid";
// PseudoCode sends no crash reports. Compiled into the native project so Observe can never reach
// Expo's ingest or the relay; `.invalid` never resolves (RFC 6761).
const observeEndpointUrl = "https://observe.invalid";

const fromRepoRoot = (relativePath: string) => `../../${relativePath}`;
const androidAdaptiveForeground = "./assets/android-icon-foreground.png";
Expand Down Expand Up @@ -498,7 +496,7 @@ const config: ExpoConfig = {
EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID,
EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME,
observability: {
tracesUrl: repoEnv.EXPO_PUBLIC_OTLP_TRACES_URL ?? "https://api.axiom.co/v1/traces",
tracesUrl: repoEnv.EXPO_PUBLIC_OTLP_TRACES_URL ?? null,
tracesDataset: repoEnv.EXPO_PUBLIC_OTLP_TRACES_DATASET ?? null,
tracesToken: repoEnv.EXPO_PUBLIC_OTLP_TRACES_TOKEN ?? null,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScroll
import * as Application from "expo-application";
import Constants from "expo-constants";
import * as Updates from "expo-updates";
import { useAtomSet, useAtomValue } from "@effect/atom-react";
import { useAtomValue } from "@effect/atom-react";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import { AsyncResult, Atom } from "effect/reactivity";
import { useState } from "react";
import { View } from "react-native";
Expand All @@ -14,12 +13,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { SymbolView } from "../../components/ui/AppSymbol";
import { AppText as Text } from "../../components/ui/AppText";
import { tryCopyTextWithHaptic } from "../../lib/copyTextWithHaptic";
import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
import { crashReportingAvailable, setCrashReportsEnabled } from "../observability/observe";
import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow";
import { SettingsScreen } from "../settings/components/SettingsScreen";
import { SettingsSection } from "../settings/components/SettingsSection";
import { SettingsSwitchRow } from "../settings/components/SettingsSwitchRow";
import {
formatStartupCrashReport,
parseStartupCrashRecords,
Expand Down Expand Up @@ -90,8 +86,6 @@ export function SettingsDiagnosticsRouteScreen() {
className="flex-1"
contentContainerClassName="gap-6 px-5 pt-4 pb-[18px]"
>
{crashReportingAvailable ? <CrashReportsSetting /> : null}

<SettingsSection title="Startup crashes">
{state.status === "loading" ? (
<View className="items-center gap-3 px-6 py-8">
Expand Down Expand Up @@ -137,54 +131,6 @@ export function SettingsDiagnosticsRouteScreen() {
);
}

function CrashReportsSetting() {
const preferences = useAtomValue(mobilePreferencesAtom);
const savePreferences = useAtomSet(updateMobilePreferencesAtom, { mode: "promiseExit" });
// One change at a time, so a slow save never turns sending back on after a later opt-out.
const [saving, setSaving] = useState(false);
const ready = AsyncResult.isSuccess(preferences) && !preferences.waiting;
const enabled = AsyncResult.isSuccess(preferences)
? preferences.value.crashReportsEnabled !== false
: true;

const change = (value: boolean) => {
setSaving(true);
// Sending stops before the save and starts only after it.
if (!value) void setCrashReportsEnabled(false);
savePreferences({ crashReportsEnabled: value })
.then((exit) => {
if (Exit.isFailure(exit)) {
console.warn("[diagnostics] could not save the crash report setting", exit.cause);
return value ? undefined : setCrashReportsEnabled(true);
}
return value ? setCrashReportsEnabled(true) : undefined;
})
.catch((error: unknown) => {
console.warn("[diagnostics] could not apply the crash report setting", error);
})
.finally(() => setSaving(false));
};

return (
<View className="gap-3">
<SettingsSection title="Crash reports">
<SettingsSwitchRow
icon="tray.and.arrow.up"
label="Send crash reports"
disabled={!ready || saving}
value={enabled}
onValueChange={change}
/>
</SettingsSection>
<Text className="px-2 text-sm leading-normal text-foreground-muted">
When the app crashes or hits an error, it sends the error message, the stack trace, the app
version, and the device model and OS version to the T3 Code team. Each launch also sends
startup timings. Reports carry a random ID for this install.
</Text>
</View>
);
}

function EmptyState(props: {
readonly icon: "exclamationmark.triangle" | "checkmark.circle";
readonly title: string;
Expand Down
64 changes: 3 additions & 61 deletions apps/mobile/src/features/observability/observe.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,5 @@
import Constants from "expo-constants";
import { Observe } from "expo-observe";

// Importing expo-observe installs its global JS error handler; reports leave only through the relay.
const relayUrl: unknown = Constants.expoConfig?.extra?.relay?.url;
const appVariant: unknown = Constants.expoConfig?.extra?.appVariant;

export const crashReportingAvailable = typeof relayUrl === "string" && relayUrl.trim() !== "";

interface PromiseRejectionTracker {
readonly enablePromiseRejectionTracker?: (options: {
readonly allRejections: boolean;
readonly onUnhandled: (id: number, rejection: unknown) => void;
}) => void;
}

// React Native tracks unhandled rejections only in development, for LogBox; Observe has no hook.
if (!__DEV__) {
const hermes: PromiseRejectionTracker | undefined = Reflect.get(globalThis, "HermesInternal");
hermes?.enablePromiseRejectionTracker?.({
allRejections: true,
onUnhandled: (_id, rejection) => Observe.reportError(rejection),
});
}

let latestConfiguration = 0;

// While sending is off, a dispatch pass drops what it finds, so one runs before turning it back on.
// Best effort: Observe skips the pass while it is backing off from a failed send.
export async function setCrashReportsEnabled(enabled: boolean) {
const configuration = ++latestConfiguration;
const dispatchingEnabled = crashReportingAvailable && enabled;
if (dispatchingEnabled) {
await Observe.dispatchEvents();
if (configuration !== latestConfiguration) return;
}
Observe.configure({
dispatchingEnabled,
...(typeof appVariant === "string" ? { environment: appVariant } : {}),
});
}

if (!crashReportingAvailable) {
void setCrashReportsEnabled(false);
} else {
// Observe keeps its last configuration across launches; configuring it off while the setting
// loads would drop the last session's native crash. Imported lazily so the persistence graph
// never runs ahead of the imports `index.ts` orders first.
const changedSinceLoad = () => latestConfiguration !== 0;
void import("../../persistence/imperative")
.then(({ loadPreferences }) => loadPreferences())
.then(
(preferences) =>
changedSinceLoad()
? undefined
: setCrashReportsEnabled(preferences.crashReportsEnabled !== false),
// Observe keeps the last choice it was given, which stands in for the unreadable one.
(error: unknown) => console.warn("[observe] could not read the crash report setting", error),
)
.catch((error: unknown) => {
console.warn("[observe] could not apply the crash report setting", error);
});
}
// PseudoCode sends no crash reports. Importing expo-observe installs its global error handler,
// and Observe keeps its last configuration across launches, so sending is switched off here.
Observe.configure({ dispatchingEnabled: false });
7 changes: 2 additions & 5 deletions apps/mobile/src/features/observability/tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ vi.mock("expo-constants", () => ({
},
}));

it.effect("does not let OTLP serialization failures alter application effects", () => {
it.effect("sends no spans even when a build carries trace credentials", () => {
const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
const layerTracing = Tracing.layerFromConfig(
{
Expand Down Expand Up @@ -52,10 +52,7 @@ it.effect("does not let OTLP serialization failures alter application effects",
Effect.scoped,
Effect.andThen(
Effect.sync(() => {
expect(fetchFn).toHaveBeenCalledOnce();
expect(new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array)).toContain(
"mobile.test.failed-span",
);
expect(fetchFn).not.toHaveBeenCalled();
}),
),
);
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import packageJson from "../package.json" with { type: "json" };
// Official release workflows set this telemetry channel; other builds report dev.
// Unlike __T3CODE_BUILD_CHANNEL__, this is not the sign-in page's latest/nightly channel.
declare const __T3CODE_TELEMETRY_CHANNEL__: ReleaseChannel | undefined;
export const telemetryChannel: ReleaseChannel =
const telemetryChannel: ReleaseChannel =
typeof __T3CODE_TELEMETRY_CHANNEL__ === "undefined" ? "dev" : __T3CODE_TELEMETRY_CHANNEL__;

export const DEFAULT_PORT = 3773;
Expand Down
Loading
Loading