npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@stoplight/reporter

v2.0.0

Published

<!-- BADGES -->

Downloads

552

Readme

@stoplight/reporter

Installation

Supported in modern browsers and node.

# latest stable
yarn add @stoplight/reporter

The Why?

If you reached this page, it is very likely that someone told you "we've got @stoplight/reporter package, use it instead, please.". You are likely to be wondering what on earth this package is, and what kind of problem it aims to solve. Although it may redundant, keep in mind we have plenty of projects, some of which share similar needs. For instance, there are Studio Desktop, Studio Web and Ninja (Platform V2). They all integrate with the same set of services. In normal circumstances, you would need to duplicate certain amount of code, and given there are separate teams actively working on the these projects, the API would most likely differ here and there causing disruptions to other engineers responsible for implementing tracking. Moreover, since we expose custom services, the payload needs to be consistent. Having that in one place makes things easier for us. Furthermore, swapping services is easier, because we have all of them in a single spot.

Usage

Before we move to actual integrations, let's start with basics. We can distinguish two kinds of consumers:

  • libraries, i.e. graphite
  • end applications, i.e. studio

What are getReporter and setReporter?

getReporter is particularly useful when it comes to libraries. This is the only way for them to access the reporter instance and actually use it. Apart from that, in the vast majority of cases, it is simply undesired to have more than one reporter attached. You would not like to send the same unhandled error twice, right? This is what Sentry does, for instance. It does not let you attaches itself twice, otherwise the reports would be duplicate. These methods aim to prevent you from doing that. How is it accomplished? More on this below.

setReporter takes 2 arguments, one of which is optional and defaults to global. export declare function setReporter(reporterInstance: IReportingAPI, scope?: object): IReportingAPI;

What it does under the hood is setting a non-enumerable, non-writable and non-configurable property on given scope with the reporter as its value. Why is setting a non-writable and non-configurable property crucial? It makes sure no one can overwrite it in any way. Moreover, the implementation uses Object.defineProperty that throws if the property already exists and is non-configurable.

You can easily check it out by doing

setReporter(myReporter);
setReporter(myOtherReporter); // throws.

Hm, hold on. Why do we actually need to set anything on global scope? Although ES Modules are meant to be executed once, we cannot naively assume this will be always the case, for a number of reasons. While, the code is usually processed once, there is a way to load it once again, i.e. by clearing require.cache. Moreover, bundling process is error-prone as well, since code might be inlined for some reason by a certain package, and Webpack or Rollup will be unaware of that and load it once again. In other words - it's just more bulletproof.

Alright, I see. What's the purpose of scope then? It's an escape trap if you do need to set another instance of reporter, for whatever reason.

setReporter(myReporter);
const myScope = {};
setReporter(myOtherReporter, myScope); // does not throw

Cool.

getReporter export declare function getReporter(scope?: object): Optional<IReportingAPI>;

This does not need that much of an explanation, as it basically retrieves the previously set reporter. If takes the scope as an optional argument, useful if you have a reporter registered on a different scope.

Having said all of that, using getReporter before setting a new reporter is optional, the final call belongs to yourself. If your resolver is very custom, you might be happy when things fail,as this will clearly indicate someone registered another reporter that may conflict with yours. When you do have a custom implementation, tailored to your needs, just set it on a custom scope. Just don't forget to pass it that scope to getReporter later on.

IReportingAPI

export declare type Payload = Dictionary<any, string>;
export interface IReportingAPI {
    error(ex: Error | IDetailedError): void;
    error(message: string, payload?: Payload): void;
    warn(message: string, payload?: Payload): void;
    info(message: string, payload?: Payload): void;
    log(message: string, payload?: Payload): void;
    debug(message: string, payload?: Payload): void;
    time(label: string): void;
    timeEnd(label: string, payload?: Payload): void;
}

As you may see, it is a kind of subset of Console API, which the majority of engineers should be familiar with.

DetailedError

This is a special abstract Error class that extends the native Error one. It has two following advantages over the native Error:

  • it is serializable (crucial for Studio, where we serialize certain errors back and forth between the renderer thread and the worker)
  • extra property that is then attached to Sentry extras when the error gets sent
Example
import { GraphiteError as GraphiteErrorType } from '@stoplight/graphite';
import { DetailedError } from '@stoplight/reporter/exceptions';

export type GraphiteErrorPayload = Pick<GraphiteErrorType, 'code' | 'nodeId' | 'trace' | 'data'>;

export class GraphiteError extends DetailedError<GraphiteErrorPayload> {
  public name = 'GraphiteError';
  public extra: GraphiteErrorPayload;

  constructor(error: GraphiteErrorType) {
    super(error.message);

    this.extra = { // this is included in Sentry whenever you throw GraphiteError
      code: error.code,
      nodeId: error.nodeId,
      data: error.data,
      trace: error.trace,
    };
  }
}

Extra methods

  • addPayloadBuilder - the builder method is called whenever a particular individual payload is about to be sent. This is very useful if you want to attach information that is specific to a given payload at the execution time. It's been inspired by beaver-logger, therefore you can check out theirs documentation either.

  • addMetaBuilder - the builder method is called:

    • once - in case of Sentry, it's supposed to clear the previously set Sentry's scope and set configure a new one which is a result of the data returned by builder,
    • when the logs are about to be flushed - in case of logger (see docs below for more info)

It's been inspired by beaver-logger, therefore you can check out theirs documentation either.

Both methods are hooked up for both Sentry and the logger instance (if it implements such). Moreover, Amplitude makes use of them as well.

It's not required for a particular Reporter to implement addPayloadBuilder and/or addMetaBuilder. They are both totally optional and serve mostly to make attaching general state information easier.

Here is an actual real-life example of usage of addPayloadBuilder

// add some global properties to each logged event
if ('addPayloadBuilder' in Reporter) {
  Reporter.addPayloadBuilder(() => {
    const payload: {
      sidebar_tree?: string;
      active_panels?: ModeId[];
      num_active_panels?: number;
    } = {};

    const studioStore = this.activeProjectStore?.studioStore;
    const graphStore = this.activeProjectStore?.graphStore;
    if (studioStore && graphStore?.state === 'activated') {
      payload.sidebar_tree = studioStore.state === 'activated' ? studioStore.uiStore.activeSidebarTree : 'na';
      payload.active_panels = studioStore.uiStore?.activeLayout?.activeModeIds || [];
      payload.num_active_panels = payload.active_panels.length;
    }

    return payload;
  });
}

Each entry in Sentry, Kibana as well as other tools (provided that it's properly integrated) will have that extra information attached to each entry / event.

This is how Sentry processes the data:

sentry-additional

Another example of addMetaBuilder

// track some global data on all logged events
if ('addMetaBuilder' in Reporter) {
  Reporter.addMetaBuilder(() => {
    const payload: any = {
      is_studio: true,
      is_desktop: process.env.RUN_CONTEXT === 'desktop',
      user_id: this.userStore.authorizedUser ? this.userStore.authorizedUser.id : null,
      // DEPRECATED: replaced by device_id, can remove in a few months
      install_id: deviceId,
    };

    const studioStore = this.activeProjectStore && this.activeProjectStore.studioStore;
    if (studioStore) {
      payload.is_git = studioStore.state === 'activated' && studioStore.gitStore.isGitRepo;
    }

    return payload;
  });
}

This is how Sentry processes the data:

sentry-additional

Browser

In the main entry-point of your project

import { setReporter } from '@stoplight/reporter';
import { BrowserReporter } from '@stoplight/reporter/integrations/browser';

setReporter(
  new BrowserReporter({
    loggerOptions: {
      url: '/endpoint-url',
      // any other valid beaver-logger option
    },
    sentryOptions: {
      environment: 'production',
      // any other valid Sentry option
    },
  }),
);

In every other place

import { getReporter } from '@stoplight/reporter';

const reporter = getReporter();

reporter.error('woops');

Contributing

  1. Clone repo.
  2. Create / checkout feature/{name}, chore/{name}, or fix/{name} branch.
  3. Install deps: yarn.
  4. Make your changes.
  5. Run tests: yarn test.prod.
  6. Stage relevant files to git.
  7. Commit: yarn commit. NOTE: Commits that don't follow the conventional format will be rejected. yarn commit creates this format for you, or you can put it together manually and then do a regular git commit.
  8. Push: git push.
  9. Open PR targeting the master branch.