Skip to content

Commit

Permalink
fix: track unsupported UA exceptions via Sentry
Browse files Browse the repository at this point in the history
  • Loading branch information
wessberg committed May 19, 2021
1 parent a58e2c5 commit e3ce123
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 23 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ These two services are very much alike. In fact, `Polyfiller` depends on the lib
`Polyfiller` exists for two reasons:

- A wider range of available polyfills such as Web Components, PointerEvents and Proxies
- Deep integration with `Caniuse`. If you use something like `babel-preset-env` with a `browserslist` and you generate this automatically based on the features you want to support with a tool such as [browserslist-generator](https://www.npmjs.com/package/@wessberg/browserslist-generator), both syntax detection for transpiling, and feature detection for polyfilling will be seamlessly based on your `browserslist`.
- Deep integration with `Caniuse`. If you use something like `babel-preset-env` with a `browserslist` and you generate this automatically based on the features you want to support with a tool such as [browserslist-generator](https://www.npmjs.com/package/browserslist-generator), both syntax detection for transpiling, and feature detection for polyfilling will be seamlessly based on your `browserslist`.

### Hosting

Expand Down
45 changes: 44 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@
"@webcomponents/shadycss": "1.10.2",
"@webcomponents/shadydom": "1.8.0",
"@webcomponents/template": "1.4.4",
"@wessberg/browserslist-generator": "1.0.47",
"@wessberg/di": "2.0.3",
"@wessberg/fileloader": "1.1.12",
"@wessberg/filesaver": "1.0.11",
Expand All @@ -96,6 +95,7 @@
"Base64": "1.1.0",
"blob-polyfill": "5.0.20210201",
"browserslist": "4.16.6",
"browserslist-generator": "1.0.48",
"chalk": "4.1.1",
"console-polyfill": "0.3.0",
"construct-style-sheets-polyfill": "2.4.16",
Expand Down
16 changes: 15 additions & 1 deletion src/api/controller/polyfill-api-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@ import {encodeFeatureSetForHttpHeader, getPolyfillRequestFromUrl} from "../../ut
import {StatusCodes} from "http-status-codes";
import {IPolyfillBl} from "../../bl/polyfill/i-polyfill-bl";
import {pickEncoding} from "../util/util";
import {IMetricsService} from "../../service/metrics/i-metrics-service";
import {generateBrowserslistFromUseragent} from "browserslist-generator";

export class PolyfillApiController {
constructor(private polyfillBl: IPolyfillBl) {}
constructor(private polyfillBl: IPolyfillBl, private metricsService: IMetricsService) {}

@GET({path: constant.endpoint.polyfill})
async onPolyfillRequested(request: ApiRequest): Promise<ApiResponse> {
// Attempt to parse the user agent into a proper Browserslist
if (request.userAgent != null) {
try {
generateBrowserslistFromUseragent(request.userAgent);
} catch (ex) {
// Un-set the user agent
request.userAgent = undefined;
// Capture the exception, but allow it
this.metricsService.captureException(ex);
}
}

// Normalize the polyfill request
const polyfillRequest = getPolyfillRequestFromUrl(request.url, request.userAgent, pickEncoding(request.acceptEncoding));

Expand Down
2 changes: 1 addition & 1 deletion src/api/server/i-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type ServerOptions = HttpServerOptions | HttpsServerOptions;
export interface ApiRequest {
request: Request;
url: URL;
userAgent: string;
userAgent: string | undefined;
cachedChecksum: string | undefined;
acceptEncoding: Set<ContentEncodingKind> | undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion src/polyfill/polyfill-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {PolyfillContext} from "./polyfill-context";
import {EcmaVersion} from "../util/type/ecma-version";

export interface PolyfillRequest {
userAgent: string;
userAgent: string | undefined;
ecmaVersion: EcmaVersion;
encoding?: ContentEncodingKind;
features: Set<PolyfillFeatureInput>;
Expand Down
2 changes: 1 addition & 1 deletion src/service/logger/logger-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class LoggerService implements ILoggerService {
/**
* The prefix to attach to log messages
*/
private readonly INFO_PREFIX: string = this.padPrefix("");
private readonly INFO_PREFIX: string = "";

constructor(private config: Config) {}

Expand Down
6 changes: 6 additions & 0 deletions src/service/metrics/i-metrics-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import {Express} from "express";

export interface IMetricsService {
readonly hasCapturedEvents: boolean;
readonly hasCapturedMessages: boolean;
readonly hasCapturedExceptions: boolean;
initialize(app: Express): Promise<void>;
configureRequestHandlers(app: Express): Promise<void>;
configureErrorHandlers(app: Express): Promise<void>;
captureEvent(event: unknown): Promise<void>;
captureMessage(message: string): Promise<void>;
captureException(exception: Error): Promise<void>;
}
28 changes: 28 additions & 0 deletions src/service/metrics/noop-metrics-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,37 @@ import {IMetricsService} from "./i-metrics-service";
import {Express} from "express";

export class NoopMetricsService implements IMetricsService {
#hasCapturedEvents = false;
#hasCapturedMessages = false;
#hasCapturedExceptions = false;

get hasCapturedEvents() {
return this.#hasCapturedEvents;
}

get hasCapturedMessages() {
return this.#hasCapturedMessages;
}

get hasCapturedExceptions() {
return this.#hasCapturedExceptions;
}

async initialize(_app: Express): Promise<void> {}

async configureRequestHandlers(_app: Express): Promise<void> {}

async configureErrorHandlers(_app: Express): Promise<void> {}

async captureEvent(_event: unknown): Promise<void> {
this.#hasCapturedEvents = true;
}

async captureException(_exception: Error): Promise<void> {
this.#hasCapturedExceptions = true;
}

async captureMessage(_message: string): Promise<void> {
this.#hasCapturedMessages = true;
}
}
44 changes: 39 additions & 5 deletions src/service/metrics/sentry-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import {IMetricsService} from "./i-metrics-service";
import {Handlers, init, Integrations} from "@sentry/node";
import {Handlers, init, Event, Integrations, captureException, captureEvent, captureMessage} from "@sentry/node";
import {Integrations as TracingIntegrations} from "@sentry/tracing";
import {Express} from "express";
import {Config} from "../../config/config";

export class SentryService implements IMetricsService {
private initialized = false;
#initialized = false;
#hasCapturedEvents = false;
#hasCapturedMessages = false;
#hasCapturedExceptions = false;

get hasCapturedEvents() {
return this.#hasCapturedEvents;
}

get hasCapturedMessages() {
return this.#hasCapturedMessages;
}

get hasCapturedExceptions() {
return this.#hasCapturedExceptions;
}

constructor(private config: Config) {}

Expand All @@ -22,19 +37,38 @@ export class SentryService implements IMetricsService {
],
tracesSampleRate: this.config.production ? 0.5 : 1.0
});
this.initialized = true;
this.#initialized = true;
}

async configureRequestHandlers(app: Express): Promise<void> {
if (!this.initialized) return;
if (!this.#initialized) return;

app.use(Handlers.requestHandler());
app.use(Handlers.tracingHandler());
}

async configureErrorHandlers(app: Express): Promise<void> {
if (!this.initialized) return;
if (!this.#initialized) return;

app.use(Handlers.errorHandler());
}

async captureEvent(event: Event): Promise<void> {
if (!this.#initialized) return;

this.#hasCapturedEvents = true;
captureEvent(event);
}
async captureMessage(message: string): Promise<void> {
if (!this.#initialized) return;

this.#hasCapturedMessages = true;
captureMessage(message);
}
async captureException(exception: Error): Promise<void> {
if (!this.#initialized) return;

this.#hasCapturedExceptions = true;
captureException(exception);
}
}
23 changes: 12 additions & 11 deletions src/util/polyfill/polyfill-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {polyfillRawForceName} from "../../polyfill/polyfill-raw-force-name";
import {polyfillOptionValueSeparator} from "../../polyfill/polyfill-option-value-separator";
import {createHash} from "crypto";
import {constant} from "../../constant/constant";
import {generateBrowserslistFromUseragent, getAppropriateEcmaVersionForBrowserslist, userAgentSupportsFeatures} from "@wessberg/browserslist-generator";
import {generateBrowserslistFromUseragent, getAppropriateEcmaVersionForBrowserslist, userAgentSupportsFeatures} from "browserslist-generator";
import {truncate} from "@wessberg/stringutil";
import {IPolyfillLibraryDictEntry, IPolyfillLocalDictEntry} from "../../polyfill/polyfill-dict";
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
Expand All @@ -23,7 +23,7 @@ import {booleanize} from "../../api/util";
* Traces all polyfill names that matches the given name. It may be an alias, and it may refer to additional aliases
* within the given features
*/
export function traceAllPolyfillNamesForPolyfillName(name: PolyfillName): Set<PolyfillDealiasedName> {
function traceAllPolyfillNamesForPolyfillName(name: PolyfillName): Set<PolyfillDealiasedName> {
// Get the PolyfillDict that matches the given name
const match = constant.polyfill[name];
// If none exists, return an empty array
Expand Down Expand Up @@ -76,8 +76,8 @@ export function getPolyfillSetIdentifier(polyfills: Set<PolyfillFeatureInput>, c
/**
* Returns true if the given polyfill should be included for a particular user agent
*/
function shouldIncludePolyfill(force: boolean, context: PolyfillContext, userAgent: string, features: string[], supportedContexts: Set<PolyfillContext>): boolean {
return supportedContexts.has(context) && (force || features.length < 1 || !userAgentSupportsFeatures(userAgent, ...features));
function shouldIncludePolyfill(force: boolean, context: PolyfillContext, userAgent: string | undefined, features: string[], supportedContexts: Set<PolyfillContext>): boolean {
return supportedContexts.has(context) && (force || features.length < 1 || userAgent == null || !userAgentSupportsFeatures(userAgent, ...features));
}

/**
Expand Down Expand Up @@ -105,7 +105,7 @@ function getEffectiveDependors(polyfillName: PolyfillDealiasedName, includedPoly
*/
function getRequiredPolyfillsForUserAgent(
polyfillSet: Set<PolyfillFeatureInput>,
userAgent: string,
userAgent: string | undefined,
context: PolyfillContext
): [PolyfillFeature[], Map<PolyfillDealiasedName, number>] {
const polyfills: PolyfillFeature[] = [];
Expand Down Expand Up @@ -155,7 +155,11 @@ function getRequiredPolyfillsForUserAgent(
/**
* Orders the polyfills given in the Set, including their dependencies
*/
export async function getOrderedPolyfillsWithDependencies(polyfillSet: Set<PolyfillFeatureInput>, userAgent: string, context: PolyfillContext): Promise<Set<PolyfillFeature>> {
export async function getOrderedPolyfillsWithDependencies(
polyfillSet: Set<PolyfillFeatureInput>,
userAgent: string | undefined,
context: PolyfillContext
): Promise<Set<PolyfillFeature>> {
const [requiredPolyfills, polyfillToIndexMap] = getRequiredPolyfillsForUserAgent(polyfillSet, userAgent, context);
const requiredPolyfillNames: Set<PolyfillDealiasedName> = new Set(polyfillToIndexMap.keys());
const requiredPolyfillNamesArray = [...requiredPolyfillNames];
Expand All @@ -176,7 +180,7 @@ export async function getOrderedPolyfillsWithDependencies(polyfillSet: Set<Polyf
/**
* Generates an IPolyfillRequest from the given URL
*/
export function getPolyfillRequestFromUrl(url: URL, userAgent: string, encoding?: ContentEncodingKind): PolyfillRequest {
export function getPolyfillRequestFromUrl(url: URL, userAgent: string | undefined, encoding?: ContentEncodingKind): PolyfillRequest {
const featuresRaw = url.searchParams.get("features");
const contextRaw = url.searchParams.get("context") as PolyfillContext;
const sourcemapRaw = url.searchParams.get("sourcemap");
Expand Down Expand Up @@ -232,7 +236,7 @@ export function getPolyfillRequestFromUrl(url: URL, userAgent: string, encoding?
// Return the IPolyfillRequest
return {
userAgent,
ecmaVersion: getAppropriateEcmaVersionForBrowserslist(generateBrowserslistFromUseragent(userAgent)),
ecmaVersion: userAgent == null ? "es5" : getAppropriateEcmaVersionForBrowserslist(generateBrowserslistFromUseragent(userAgent)),
encoding,
features: featureSet,
context,
Expand All @@ -243,9 +247,6 @@ export function getPolyfillRequestFromUrl(url: URL, userAgent: string, encoding?

/**
* Encodes a PolyfillSet such that it can be embedded in a HTTP header
*
* @param polyfillSet
* @returns
*/
export function encodeFeatureSetForHttpHeader(polyfillSet: Set<PolyfillFeature>): string {
return truncate(
Expand Down
Loading

0 comments on commit e3ce123

Please sign in to comment.