← Back to Blog Home

Logging in React Native with Sentry

Logging in React Native with Sentry

Logs are often the first place dev teams look when they investigate an issue. But logs are often added as an afterthought, and developers struggle with the balance of logging too much or too little.

As a seasoned developer, you may remember a time when you were asked to investigate an issue and then handed a 200 MB plaintext log file. Three hours and four Python scripts later, you would realize that the problem was in a different component.

Over time, many standards and libraries have been developed to make logging easier, and React Native has plenty of logging functions. Our initial guide to logging in React Native is a good starting point. However, if you want to take things to the next level, this guide shows you how to use Sentry’s logging features to ensure your logs are useful and all the information you need is readily available.

Getting Logs into Sentry

This guide uses a simple React Native (Expo) app to show examples of Sentry’s various logging features. The app is a contact form with some basic validation on the fields. If you want to follow along and try for yourself, you can find the app in the demo repository.

Contact form interface with fields for name, email, and message, and a submit button displayed in a browser window on localhost.

Demo React Native app interface

If you have an existing React Native application, or plan to build one, this guide provides the steps required to take full advantage of logging with Sentry.

Set up

To start using Sentry features, we first need to install Sentry into our project. Start by creating a new project in Sentry and choosing React Native as the platform. Then, run the installation wizard to set up the necessary configurations in your local project:

npx @sentry/wizard@latest -i reactNative --saas --org your-team-name --project your-project-name

Enable logging when prompted. When the configuration completes, you should see code like the following added to your main App.js class:

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Enable logs to be sent to Sentry
  enableLogs: true,
});

This initializes the Sentry SDK and enables logging with the Sentry logging library.

Sentry Logger API

Now that we’ve initialized Sentry and enabled logs, we can use the Sentry.logger namespace to send logs to our Sentry dashboard.

We can log messages with different log levels:

  • trace
  • debug
  • info
  • warn
  • error
  • fatal

We can also use the Sentry.logger.fmt function to add properties to the log message.

In the example app, add a new log call to the handleSubmit function:

const handleSubmit = () => {
    Sentry.logger.info(
      `${name} submitted a form.`
    );

    //...

After we click the Submit button, we will see an info level log on our Sentry dashboard.

Logs dashboard showing a single log entry confirming a form submission, with a timestamped message reading “Lewis submitted a form” and a bar chart indicating one log event.

We can manually add additional attributes to the logs. These don’t form part of the main log message but instead attach to the log and allow us to search or filter by them later.

To try this out, update the log that we added previously:

Sentry.logger.info(
    `${name} submitted a form.`,
    {
        filterID: "01",
        extraMessage: "This is an extra message."
    }
);

When we submit our form, we will see both the new filterID and extraMessage fields in the log payload.

Detailed log entry view showing a form submission by James, with metadata including browser, environment, SDK version, and highlighted custom fields for an extra message and filter ID.

Integrations

We have the Sentry Logger API working, but our app already has logging built with the default JavaScript console object. Luckily, we don’t have to rewrite all our logging because Sentry also provides a way to integrate with default JavaScript logging.

Update the Sentry.init call with the following code after the enableLogs: true line in App.js:

integrations: [
    Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
],

This causes all our existing logging to be sent to our Sentry. We can exclude certain log levels if we like. For example, if we want to reduce clutter on our dashboard, we could opt to send only error level logs.

Sentry.consoleLoggingIntegration({ levels: ["error"] }),

However, as you’ll see later in this guide, we can also reduce clutter using the filtering in the dashboard itself, so it’s often better to send Sentry as much useful information as possible.

Let’s restart the app, fill in the form, and take a look at the logs that are sent.

Logs dashboard showing multiple form-related log events over time, including submission started, successful submission with payload data, form reset, and application startup, with a bar chart summarizing log counts.

Even with these integrated logs, Sentry links all the extra information available, such as the originating environment and browser type. All these data points can be used to filter, search, or group our logs.

Searching, filtering, and grouping

The Logs dashboard gives us access to all our logs in one place, as well as to tools for finding exactly what we need.

At the top of the dashboard, we can use the search bar to find specific log messages. We can search for text that appears in the log message itself or use Sentry’s query syntax to search by specific fields.

For example, we can get all the logs relating to submitting a form by typing sub in the search bar.

Logs dashboard filtered by the search query "sub" for form submissions, showing multiple matching log entries highlighted in the results table and a bar chart of matching log counts over time.

We can also search by log level. To see only warning logs, use the query severity:warn in the search bar.

Logs dashboard filtered to warning-level events, showing validation failure messages for invalid email format and missing required form fields, with a timeline chart of warning log activity.

We can also filter by the extra attributes that we added earlier. Type filterID:01 in the search bar.

Logs dashboard filtered by a custom filter ID, showing a detailed form submission log with highlighted metadata fields, including filter ID and extra message, alongside a timeline of matching log events.

When we have logs coming from multiple parts of our application, we can use custom attributes to quickly isolate logs from specific components or flows.

We can combine multiple filters together. For example, we could filter by both filterID and log level to see only warning logs from a specific part of our app.

Sentry also lets us group logs by different attributes. To do so, click the >> Advanced button, then open the Group By dropdown and select the type severity.

Logs dashboard with an aggregated view grouped by severity, showing a bar chart of log counts over time and a summary table listing info and warning log totals.

Our logs are now organized by their log level, making it easier to see how many logs we have at each severity. We can also group by environment, browser type, or any custom attribute we’ve added to our logs.

This becomes particularly useful when debugging an issue that affects specific users or environments. We can quickly group by browser or operating system to see if a problem is isolated to certain platforms.

Debugging a real application

To see the real value of Sentry logging, let’s look at a more complete example. We’ve built a cat voting app, where users can upvote or downvote cat pictures. The app has a React Native frontend that talks to an Express.js backend with a SQLite database. The frontend fetches cat images from an external API and stores them in the database.

If you want to explore the logging features for yourself, you can clone the app repository.

We’ve only set up Sentry on the frontend, as this guide is focused on React Native. We could, and should, create an Express.js Sentry project for our backend code as well.

The app structure

The app consists of:

  • A voting screen that displays cat images with upvote and downvote buttons
  • A winner screen that shows the most popular cat
  • A context provider that manages data fetching and state
  • An API service that handles all backend communication

This is what the Sentry setup looks like in the frontend/App.js file:

Sentry.init({
    dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
    environment: __DEV__ ? 'development' : 'production',
    sendDefaultPii: true,
    enableLogs: true,
    tracesSampleRate: 1.0,
});

Tracking user actions

In frontend/src/screens/CatListScreen.js, we log when users vote on cats. We use both the logger API and breadcrumbs to create a trail of what the user does:

const handleVote = async (catId, voteType) => {
    Sentry.logger.info("User clicked vote button", {
      catId: catId.toString(),
      voteType,
      feature: "voting",
    });

    Sentry.withScope((scope) => {
      scope.setTag('feature', 'voting');
      scope.setTag('voteType', voteType);

      Sentry.addBreadcrumb({
        category: 'user-action',
        message: `User initiating ${voteType} for cat ${catId}`,
        level: 'info',
      });

      try {
        submitVote(catId, voteType);
        Sentry.logger.info("Vote completed successfully", {
          catId: catId.toString(),
          voteType,
        });
      } catch (err) {
        Sentry.logger.error("Vote failed", {
          catId: catId.toString(),
          voteType,
          errorMessage: err.message,
        });
        Sentry.captureException(err);
      }
    });
};

Debugging a backend issue

Let’s suppose some users report getting a 500 Internal Server Error (an API error) when they vote on specific cats. We turn to our Logs dashboard to investigate.

First, we check the dashboard for recent error logs. We filter by severity:error and see several Vote submission failed logs. We click on one of them to see the details.

Logs dashboard filtered to error-level events, showing a failed vote submission with detailed error metadata including API error code, custom fields like cat ID and vote type, and a timeline of error activity.

The log shows us:

  • The cat the user tried to vote on (catId: "133")
  • The type of vote it was (voteType: "upvote")
  • The error message from the backend (errorMessage: "API Error: 500")
  • The timestamp of when the error happened

Because this is an error level event, Sentry also logs an Issue. We navigate to our Issues dashboard and click on the corresponding error event.

Feed view showing a single unresolved error issue, “API Error: 500,” with project name, stack trace reference, and metadata such as last seen time, event count, and affected users.

In the issue entry, we can scroll down and see the breadcrumb trail leading up to the error. The logs show the user loaded the cat list, clicked the upvote button, and then the API request failed with a 500 HTTP status code.

Error details view showing breadcrumbs leading up to an API Error 500, including failed POST requests to the votes endpoint, fetch errors, and user action context for an upvote attempt.

Now we know the problem is on the backend. We check the backend server logs around the same timestamp and find a database constraint error. The backend attempted to insert a vote, but the database rejected it due to a foreign key constraint violation (the cat ID doesn’t exist in the database).

Looking back at the Sentry logs, we search for when this cat was added. We filter by operation:fetchCats and see that the app fetched cats from the external API, but when it tried to save them to the database, one of the requests failed. The Sentry log shows:

Failed to fetch cats
errorMessage: "Database error: unique constraint failed"

The problem is clear: When the app fetched new cats, some were successfully added to the database while others failed due to duplicate IDs. Users could see cats that weren’t in the database, and when they tried to vote on those cats, the backend rejected the vote.

We can fix the issue by improving error handling in the cat fetching logic. When cats fail to save, we remove them from the UI so that users can’t vote on cats aren’t in the database:

const fetchCats = async () => {

    ...

    const response = await fetch('https://api.thecatapi.com/v1/images/search?limit=10');
    const newCats = await response.json();

    // Try to save cats to backend
    const saveResponse = await api.post('/api/cats', { cats: newCats });

    // Get all cats again - this only includes successfully saved cats
    const updatedCats = await api.get('/api/cats');
    setCats(updatedCats);

    // Log if some cats failed to save
    if (updatedCats.length < newCats.length) {
        Sentry.logger.warn("Some cats failed to save to database", {
            fetched: newCats.length,
            saved: updatedCats.length,
            failed: newCats.length - updatedCats.length,
        });
    }
};

Performance monitoring

In addition to errors, we can also track performance using Sentry’s spans feature. In frontend/src/context/CatsContext.js, we can monitor how long it takes to load cats:

const fetchCats = async () => {
    const span = Sentry.startInactiveSpan({
      op: 'db.query',
      name: 'fetchCats',
    });

    try {
      const catsData = await api.get('/api/cats');

      span.setAttributes({
        count: catsData.length,
        source: "database",
      });

      span.setStatus({ code: 1 }); // OK status
    } catch (error) {
      span.setStatus({ code: 2, message: error.message }); // ERROR status
      throw error;
    } finally {
      span.end();
    }
};

Using spans provides better developer experience for performance tracking. You can view span data in the Performance tab of your Sentry dashboard, where you can easily identify slow operations and bottlenecks in your application.

The beforeSendLog function

We can use the beforeSendLog function to filter logs before they’re sent to Sentry. This is useful for controlling exactly which data reaches Sentry, helping us balance between having enough logs and not overwhelming our dashboard with noise.

For example, we can filter out debug-level logs in production, or remove sensitive information before sending logs to Sentry:

Sentry.init({
    dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
    environment: __DEV__ ? 'development' : 'production',
    enableLogs: true,

    beforeSendLog: (log) => {
        // Filter out debug logs in production
        if (!__DEV__ && log.severity === 'debug') {
            return null;
        }

        // Remove sensitive attributes
        if (log.attributes?.userEmail) {
            delete log.attributes.userEmail;
        }

        return log;
    },
});

By returning null, we prevent the log from being sent to Sentry. This approach helps us maintain clean, relevant logs while protecting sensitive user information.

Going beyond Logs

Sentry logging turns logs from raw data into a useful and intuitive tool that could save us countless hours when diagnosing an issue. If you are still unclear about any of the steps required to set up Sentry’s logging features, you can turn to our documentation on setting up logs or setting up drains and forwarders.

As we touched on in this guide, logging goes hand-in-hand with Sentry’s other features, such as tracing and profiling.

With all these features set up, you can begin to take full advantage of the Sentry toolbox.

FAQs

Can I attach structured data to logs instead of cramming everything into the message?

Yes, and you should. Adding attributes (like catId, voteType, or feature) keeps log messages readable while still making logs searchable and filterable. Think of the log message as the headline, and attributes as the fine print you’ll be grateful for during debugging.

How do logs relate to Issues in Sentry?

Logs and Issues are connected but serve different purposes. Logs help you understand what happened and in what order. Issues represent actionable failures, usually tied to errors or crashes. When an error-level log is recorded, Sentry can create or update an Issue, linking your logs, breadcrumbs, stack traces, and user context together in one place.

How do logs help when debugging issues that span frontend and backend services?

Logs provide the narrative, while tracing provides the timeline. When you log meaningful events on the frontend and combine them with backend logs and traces, you can follow a request from a user tap all the way to a database error. This is especially useful when a frontend error turns out to be a backend problem wearing a different hat.

Should I log everything and filter later, or be selective from the start?

Generally, log generously but intentionally. It’s easier to filter noisy logs in Sentry than to retroactively add missing ones after an incident. Focus on logging state changes, user actions, and system boundaries (API calls, persistence, async work). If volume becomes a problem, refine with log levels or beforeSendLog.

When should I use logs versus breadcrumbs?

Use logs when you want something searchable and queryable on its own. Use breadcrumbs when you want context attached to an error or crash. Logs tell you what happened across the app. Breadcrumbs tell you what happened right before something broke.

What’s the next step after setting up logging?

Start correlating logs with errors, performance spans, and releases. Logging is most powerful when it’s part of a larger feedback loop, helping you understand how code changes affect real users, not just whether the app crashed.

Syntax.fm logo

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax