homeresume
 
   
🔍

Error tracking with Sentry

February 14, 2023

Error tracking and alerting are crucial in the production environment, proactively fixing the errors leads to a better user experience. Sentry is one of the error tracking services, and it provides alerting for unhandled exceptions. You should receive an email when something wrong happens.

Sentry issues show the error stack trace, device, operating system, and browser information. The project dashboard shows an unhandled exception once it's thrown. This post covers the integration of several technologies with Sentry.

Node.js

  • Create a Node.js project on Sentry

  • Install the package

npm i @sentry/node
  • Run the following script
const Sentry = require('@sentry/node');
Sentry.init({
dsn: SENTRY_DSN
});
test();

Next.js

  • Create a Next.js project on Sentry (version 13 is not yet supported)

  • Run the following commands for the setup

npm i @sentry/nextjs
npx @sentry/wizard -i nextjs

Gatsby

  • Create a Gatsby project on Sentry

  • Install the package

npm i @sentry/gatsby
  • Add plugin in Gatsby config
module.exports = {
plugins: [
// ...
{
resolve: '@sentry/gatsby',
options: {
dsn: SENTRY_DSN
}
}
]
};

React Native

  • Create a React Native project on Sentry

  • Run the following commands for the setup

npm i @sentry/react-native
npx @sentry/wizard -i reactNative -p android
2022

Push notifications with Firebase

May 29, 2022

Push notifications are a great alternative to email notifications. There is no need for a verification step, UX is improved, and user engagement with the app is increased. This post covers both front-end and back-end setups.

Requirements for the push notifications

  • firebase package installed
  • Created Firebase project
  • Firebase configuration can be found on Project Overview Project settings General Your apps
  • Public Vapid key can be found on Project Overview Project settings Cloud Messaging Web Push certificates (used on the front-end)
  • Firebase messaging service worker
  • Project ID can be found on Project Overview Project settings General tab
  • Server key for sending the push notifications (used on the back end)
  • HTTPS connection (localhost for local development)

Front-end setup

Firebase messaging service worker

The following service worker should be registered for handling background notifications. A custom notificationclick handler should be implemented before importing firebase libraries. The below implementation opens a new window with the defined URL if it is not already open. Firebase automatically checks for service workers at /firebase-messaging-sw.js, so it should be publicly available.

// /firebase-messaging-sw.js
/* eslint-disable no-unused-vars */
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const DEFAULT_URL = '<URL>';
const url =
event.notification?.data?.FCM_MSG?.notification?.click_action ||
DEFAULT_URL;
event.waitUntil(
clients.matchAll({ type: 'window' }).then((clientsArray) => {
const hadWindowToFocus = clientsArray.some((windowClient) =>
windowClient.url === url ? (windowClient.focus(), true) : false
);
if (!hadWindowToFocus)
clients
.openWindow(url)
.then((windowClient) => (windowClient ? windowClient.focus() : null));
})
);
});
importScripts('https://www.gstatic.com/firebasejs/9.19.1/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.19.1/firebase-messaging-compat.js');
const firebaseApp = initializeApp({
apiKey: 'xxxxxx',
authDomain: 'xxxxxx',
projectId: 'xxxxxx',
storageBucket: 'xxxxxx',
messagingSenderId: 'xxxxxx',
appId: 'xxxxxx',
measurementId: 'xxxxxx'
});
const messaging = getMessaging(firebaseApp);

Helper functions

getToken
  • generates a unique registration token for the browser or gets an already generated token
  • requests permission to receive push notifications
  • triggers the Firebase messaging service worker

There are multiple types of errors as response:

  • code messaging/permission-blocked - user blocks the notifications
  • code messaging/unsupported-browser - user's browser doesn't support the APIs required to use the Firebase SDK
  • code messaging/failed-service-worker-registration - there's an issue with the Firebase messaging service worker

The access token is invalidated when a user manually blocks the notifications in the browser settings.

isSupported
  • checks if all required APIs for push notifications are supported
  • returns Promise<boolean>

It should be used in useEffect hooks.

import { isSupported } from 'firebase/messaging';
// ...
useEffect(() => {
isSupported()
.then((isAvailable) => {
if (isAvailable) {
// ...
}
})
.catch(console.error);
}, []);
// ...
initializeApp
  • should be called before the app starts
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken } from 'firebase/messaging';
import { firebaseConfig } from 'constants/config';
export const initializeFirebase = () => initializeApp(firebaseConfig);
export const getTokenForPushNotifications = async () => {
const messaging = getMessaging();
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY
});
return token;
};

Back-end setup

Server keys

The server key for API v1 can be derived from the service account key JSON file. In that case, the JSON file should be encoded and stored in the environment variable to prevent exposing credentials in the repository codebase. The service account key JSON file can be downloaded by clicking Generate new private key on the Project Overview Project settings Service accounts tab.

import * as serviceAccountKey from './service-account-key.json';
const encodedServiceAccountKey = Buffer.from(
JSON.stringify(serviceAccountKey)
).toString('base64');
process.env.SERVICE_ACCOUNT_KEY = encodedServiceAccountKey;
import 'dotenv/config';
import * as googleAuth from 'google-auth-library';
(async () => {
const serviceAccountKeyEncoded = process.env.SERVICE_ACCOUNT_KEY;
const serviceAccountKeyDecoded = JSON.parse(
Buffer.from(serviceAccountKeyEncoded, 'base64').toString('ascii')
);
const jwt = new googleAuth.JWT(
serviceAccountKeyDecoded.client_email,
null,
serviceAccountKeyDecoded.private_key,
['https://www.googleapis.com/auth/firebase.messaging'],
null
);
const tokens = await jwt.authorize();
const authorizationHeader = `Bearer ${tokens.access_token}`;
console.log(authorizationHeader);
})();

Manually sending the push notification

The icon URL should be covered with HTTPS, so the notification correctly shows it.

  • API v1
// ...
try {
const response = await axios.post(
`https://fcm.googleapis.com/v1/projects/${projectId}/messages:send`,
{
message: {
notification: {
title: 'Push notifications with Firebase',
body: 'Push notifications with Firebase body'
},
webpush: {
fcmOptions: {
link: 'http://localhost:3000'
},
notification: {
icon: 'https://picsum.photos/200'
}
},
token: registrationToken
}
},
{
headers: {
Authorization: authorizationHeader
}
}
);
console.log(response.data);
} catch (error) {
console.error(error?.response?.data?.error);
}
// ...

A successful response returns an object with name key, which presents the notification id in the format projects/{project_id}/messages/{message_id}.

There are multiple types of errors in the response:

  • code 400 - request body is not valid
  • code 401 - the derived token is expired
  • code 404 - registration token was not found

Demo

The demo with the mentioned examples is available here.

State management with Next.js and React

May 15, 2022

The global state can be helpful when components share some common parts. Also, some parts can stay persistent (in local storage) and be used in the following user's session. React provides a native way to handle state management using context with the hooks.

Usage

// ...
import { useAppContext } from "context";
import { UPDATE_FEATURE_ACTIVATION } from "context/constants";
export function CustomComponent() {
const { state, dispatch } = useAppContext();
// get value from the store
console.log(state.isFeatureActivated);
// dispatch action to change the state
dispatch({ type: UPDATE_FEATURE_ACTIVATION, payload: { isFeatureActivated: true } });
// ...
}

Context setup

// context/index.jsx
import PropTypes from "prop-types";
import React, {
createContext,
useContext,
useEffect,
useMemo,
useReducer,
} from "react";
import { getItem, setItem, STATE_KEY } from "utils/local-storage";
import { INITIALIZE_STORE } from "./constants";
import { appReducer, initialState } from "./reducer";
const appContext = createContext(initialState);
export function AppWrapper({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
const contextValue = useMemo(() => {
return { state, dispatch };
}, [state, dispatch]);
useEffect(() => {
const stateItem = getItem(STATE_KEY);
if (!stateItem) return;
const parsedState = JSON.parse(stateItem);
const updatedState = {
...initialState,
// persistent state
isFeatureActivated: parsedState.isFeatureActivated,
};
dispatch({
type: INITIALIZE_STORE,
payload: updatedState,
});
}, []);
useEffect(() => {
if (state !== initialState) {
setItem(STATE_KEY, JSON.stringify(state));
}
}, [state]);
return (
<appContext.Provider value={contextValue}>{children}</appContext.Provider>
);
}
AppWrapper.propTypes = {
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
};
export function useAppContext() {
return useContext(appContext);
}

Reducer with actions

// context/reducer.js
import { INITIALIZE_STORE, UPDATE_FEATURE_ACTIVATION } from "./constants";
export const initialState = {
isFeatureActivated: false,
};
export const appReducer = (state, action) => {
switch (action.type) {
case INITIALIZE_STORE: {
return action.payload;
}
case UPDATE_FEATURE_ACTIVATION: {
return {
...state,
isFeatureActivated: action.payload.isFeatureActivated,
};
}
default:
return state;
}
};

Wrapper around the app

// app/layout.jsx
import { AppContextProvider } from "context";
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
// ...
</head>
<body>
<AppContextProvider>{children}</AppContextProvider>
</body>
</html>
);
}

Constants

// context/constants.js
export const INITIALIZE_STORE = "INITIALIZE_STORE";
export const UPDATE_FEATURE_ACTIVATION = "UPDATE_FEATURE_ACTIVATION";

Boilerplate

Here is the link to the boilerplate I use for the development. It contains the examples mentioned above with more details.

Next.js app in production

March 6, 2022

Consider the following things when the Next.js app is running in production.

  • Error tracking and alerting

Error tracking and alerting are crucial in the production environment, proactively fixing the errors leads to a better user experience. Sentry is one of the error tracking services, and it provides alerting for unhandled exceptions. You should receive an email when something wrong happens.

Sentry issues show the error stack trace, device, operating system, and browser information. The project dashboard shows an unhandled exception once it's thrown. Run the following commands for integration with the Next.js app.

npm i @sentry/nextjs
npx @sentry/wizard -i nextjs
  • Intl API is not supported in some browsers. Cover it with a fallback.

  • localStorage API is not available when cookies are blocked. Cover it with a fallback.

Boilerplate

Here is the link to the boilerplate I use for the development.

Integrating Next.js app with Google analytics 4

February 4, 2022

Google analytics helps get more insights into app usage.

Prerequisites

  • Google analytics product is already set up, tracking ID is needed
  • Next.js app should be bootstrapped
  • react-ga4 package is installed

Analytics initialization

Analytics should be initialized inside pages/_app.js file.

import ReactGA from "react-ga4";
// ...
if (isEnvironment("production")) {
ReactGA.initialize(ANALYTICS_TRACKING_ID);
}

Tracking events (clicks, e.g.)

// ...
export function trackEvent(category, label, action = "click") {
if (isEnvironment("production")) {
ReactGA.event({
action,
category,
label,
});
}
}
// ...
<Button onClick={() => trackEvent("category", "label")}>

Boilerplate

Here is the link to the boilerplate I use for the development.

2021