Skip to content

Commit

Permalink
feat(next/swc): setup native next-swc crash reporter (#38076)
Browse files Browse the repository at this point in the history
This PR attempts to setup native crash reporter for `next-swc`. Currently, it uses sentry internally, but it is subject to change depending on the usecase & needs. In any case it won't be breaking changes since this is not transparent to the end users.

PR sets up basic, minimal setup to collect crash reports only at the moment. We may want to expand & collect more data in native next-swc, but it is not clear what we need to collect / and I believe most cases next.js's js context can collect those data via existing telemetry. Crash report is an exception native handler can perform much better by having it in native context directly. While this is sent to different endpoint than telemetry, it is considered as same opt-in configuration. If telemetry is disabled, crash reporter won't collect as well. 

The information collected by the reporter is minimally configured by sentry's sdk. These are the informations collected for example: 

- device arch / family / model
- os kernel version / name / version
- runtime (rust) version / channel
- sentry sdk
- panic backtrace
- next.js release version
- device host name

There's no per-system uuid configurations yet.

It may need some audit if we need to omit some data included in above, while most of them seems ok to me.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)
  • Loading branch information
kwonoj committed Jun 28, 2022
1 parent a3ffd1c commit 42e3365
Show file tree
Hide file tree
Showing 9 changed files with 796 additions and 6 deletions.
685 changes: 682 additions & 3 deletions packages/next-swc/Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/next-swc/crates/napi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ tracing-subscriber = "0.3.9"
tracing-chrome = "0.5.0"
wasmer = { version = "2.3.0", optional = true, default-features = false }
wasmer-wasi = { version = "2.3.0", optional = true, default-features = false }
sentry = "0.27.0"

[build-dependencies]
napi-build = "1"
serde = "1"
serde_json = "1"
19 changes: 19 additions & 0 deletions packages/next-swc/crates/napi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use std::{
extern crate napi_build;

fn main() {
// Emit current platform's target-triple into a text file to create static const
// in util.rs
let out_dir = env::var("OUT_DIR").expect("Outdir should exist");
let dest_path = Path::new(&out_dir).join("triple.txt");
let mut f =
Expand All @@ -19,5 +21,22 @@ fn main() {
)
.expect("Failed to write target triple text");

// Emit current package.json's version field into a text file to create static
// const in util.rs This is being used to set correct release version for
// the sentry's crash reporter.
let pkg_file =
File::open(Path::new("../../package.json")).expect("Should able to open package.json");
let json: serde_json::Value = serde_json::from_reader(pkg_file).unwrap();
let pkg_version_dest_path = Path::new(&out_dir).join("package.txt");
let mut package_version_writer = BufWriter::new(
File::create(&pkg_version_dest_path).expect("Failed to create package version text"),
);
write!(
package_version_writer,
"{}",
json["version"].as_str().unwrap()
)
.expect("Failed to write target triple text");

napi_build::setup();
}
3 changes: 3 additions & 0 deletions packages/next-swc/crates/napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ fn init(mut exports: JsObject) -> napi::Result<()> {
)?;
exports.create_named_method("teardownTraceSubscriber", util::teardown_trace_subscriber)?;

exports.create_named_method("initCrashReporter", util::init_crash_reporter)?;
exports.create_named_method("teardownCrashReporter", util::teardown_crash_reporter)?;

Ok(())
}

Expand Down
48 changes: 47 additions & 1 deletion packages/next-swc/crates/napi/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@ DEALINGS IN THE SOFTWARE.

use anyhow::{anyhow, Context, Error};
use napi::{CallContext, Env, JsBuffer, JsExternal, JsString, JsUndefined, JsUnknown, Status};
use sentry::{types::Dsn, ClientInitGuard, ClientOptions};
use serde::de::DeserializeOwned;
use std::{any::type_name, cell::RefCell, convert::TryFrom, path::PathBuf};
use std::{
any::type_name, borrow::Cow, cell::RefCell, convert::TryFrom, env, path::PathBuf, str::FromStr,
};
use tracing_chrome::{ChromeLayerBuilder, FlushGuard};
use tracing_subscriber::{filter, prelude::*, util::SubscriberInitExt, Layer};

static TARGET_TRIPLE: &str = include_str!(concat!(env!("OUT_DIR"), "/triple.txt"));
static PACKAGE_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/package.txt"));

#[contextless_function]
pub fn get_target_triple(env: Env) -> napi::ContextlessResult<JsString> {
env.create_string(TARGET_TRIPLE).map(Some)
Expand Down Expand Up @@ -144,3 +149,44 @@ pub fn teardown_trace_subscriber(cx: CallContext) -> napi::Result<JsUndefined> {
}
cx.env.get_undefined()
}

/// Initialize crash reporter to collect unexpected native next-swc crashes.
#[js_function(1)]
pub fn init_crash_reporter(cx: CallContext) -> napi::Result<JsExternal> {
// Attempts to follow https://nextjs.org/telemetry's debug behavior.
// However, this is techinically not identical to the behavior of the telemetry
// itself as sentry's debug option does not provides full payuload output.
let debug = env::var("NEXT_TELEMETRY_DEBUG").map_or_else(|_| false, |v| v == "1");
let dsn = if debug {
None
} else {
Dsn::from_str("https://7619e5990e3045cda747e50e6ed087a7@o205439.ingest.sentry.io/6528434")
.ok()
};

let guard = sentry::init(ClientOptions {
release: Some(Cow::Borrowed(PACKAGE_VERSION)),
dsn,
debug,
..Default::default()
});

let guard_cell = RefCell::new(Some(guard));
cx.env.create_external(guard_cell, None)
}

/// Trying to drop crash reporter guard if exists. This is the way to hold
/// guards to not to be dropped immediately after crash reporter is initialized
/// in napi context.
#[js_function(1)]
pub fn teardown_crash_reporter(cx: CallContext) -> napi::Result<JsUndefined> {
let guard_external = cx.get::<JsExternal>(0)?;
let guard_cell = &*cx
.env
.get_value_external::<RefCell<Option<ClientInitGuard>>>(&guard_external)?;

if let Some(guard) = guard_cell.take() {
drop(guard);
}
cx.env.get_undefined()
}
7 changes: 6 additions & 1 deletion packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ import { TelemetryPlugin } from './webpack/plugins/telemetry-plugin'
import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import { recursiveCopy } from '../lib/recursive-copy'
import { recursiveReadDir } from '../lib/recursive-readdir'
import { lockfilePatchPromise, teardownTraceSubscriber } from './swc'
import {
lockfilePatchPromise,
teardownTraceSubscriber,
teardownCrashReporter,
} from './swc'
import { injectedClientEntries } from './webpack/plugins/client-entry-plugin'
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
import { flatReaddir } from '../lib/flat-readdir'
Expand Down Expand Up @@ -2326,6 +2330,7 @@ export default async function build(
// Ensure all traces are flushed before finishing the command
await flushAllTraces()
teardownTraceSubscriber()
teardownCrashReporter()
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/next/build/output/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import createStore from 'next/dist/compiled/unistore'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import { flushAllTraces } from '../../trace'
import { teardownTraceSubscriber } from '../swc'
import { teardownCrashReporter, teardownTraceSubscriber } from '../swc'
import * as Log from './log'

export type OutputState =
Expand Down Expand Up @@ -92,6 +92,7 @@ store.subscribe((state) => {
// Ensure traces are flushed after each compile in development mode
flushAllTraces()
teardownTraceSubscriber()
teardownCrashReporter()
return
}

Expand Down Expand Up @@ -119,6 +120,7 @@ store.subscribe((state) => {
// Ensure traces are flushed after each compile in development mode
flushAllTraces()
teardownTraceSubscriber()
teardownCrashReporter()
return
}

Expand All @@ -135,4 +137,5 @@ store.subscribe((state) => {
// Ensure traces are flushed after each compile in development mode
flushAllTraces()
teardownTraceSubscriber()
teardownCrashReporter()
})
1 change: 1 addition & 0 deletions packages/next/build/swc/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export function parse(src: string, options: any): any
export const lockfilePatchPromise: { cur?: Promise<void> }
export function initCustomTraceSubscriber(traceFileName?: string): void
export function teardownTraceSubscriber(): void
export function teardownCrashReporter(): void
export function loadBindings(): Promise<void>
31 changes: 31 additions & 0 deletions packages/next/build/swc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { eventSwcLoadFailure } from '../../telemetry/events/swc-load-failure'
import { patchIncorrectLockfile } from '../../lib/patch-incorrect-lockfile'
import { downloadWasmSwc } from '../../lib/download-wasm-swc'
import { version as nextVersion } from 'next/package.json'
import { Telemetry } from '../../telemetry/storage'

const ArchName = arch()
const PlatformName = platform()
Expand All @@ -18,6 +19,7 @@ let wasmBindings
let downloadWasmPromise
let pendingBindings
let swcTraceFlushGuard
let swcCrashReporterFlushGuard
export const lockfilePatchPromise = {}

export async function loadBindings() {
Expand Down Expand Up @@ -215,6 +217,17 @@ function loadNative() {
}

if (bindings) {
// Initialize crash reporter, as earliest as possible from any point of import.
// The first-time import to next-swc is not predicatble in the import tree of next.js, which makes
// we can't rely on explicit manual initialization as similar to trace reporter.
if (!swcCrashReporterFlushGuard) {
// Crash reports in next-swc should be treated in the same way we treat telemetry to opt out.
let telemetry = new Telemetry({ distDir: process.cwd() })
if (telemetry.isEnabled) {
swcCrashReporterFlushGuard = bindings.initCrashReporter?.()
}
}

nativeBindings = {
isWasm: false,
transform(src, options) {
Expand Down Expand Up @@ -278,6 +291,7 @@ function loadNative() {
getTargetTriple: bindings.getTargetTriple,
initCustomTraceSubscriber: bindings.initCustomTraceSubscriber,
teardownTraceSubscriber: bindings.teardownTraceSubscriber,
teardownCrashReporter: bindings.teardownCrashReporter,
}
return nativeBindings
}
Expand Down Expand Up @@ -377,3 +391,20 @@ export const teardownTraceSubscriber = (() => {
}
}
})()

export const teardownCrashReporter = (() => {
let flushed = false
return () => {
if (!flushed) {
flushed = true
try {
let bindings = loadNative()
if (swcCrashReporterFlushGuard) {
bindings.teardownCrashReporter(swcCrashReporterFlushGuard)
}
} catch (e) {
// Suppress exceptions, this fn allows to fail to load native bindings
}
}
}
})()

0 comments on commit 42e3365

Please sign in to comment.