Skip to content

Commit c03831a

Browse files
authored
Merge branch 'main' into l10n_automation
2 parents 1262a5a + a976c83 commit c03831a

File tree

8 files changed

+212
-32
lines changed

8 files changed

+212
-32
lines changed

package-lock.json

Lines changed: 29 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"next-auth": "^4.24.13",
9393
"nodemailer": "^7.0.11",
9494
"pg": "^8.16.3",
95+
"prom-client": "^15.1.3",
9596
"raw-loader": "^4.0.2",
9697
"react": "^19.2.3",
9798
"react-aria": "^3.45.0",
Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
export const runtime = "nodejs";
6+
17
/* This Source Code Form is subject to the terms of the Mozilla Public
28
* License, v. 2.0. If a copy of the MPL was not distributed with this
39
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
410

511
import { NextRequest, NextResponse } from "next/server";
6-
import { captureMessage } from "@sentry/node";
712

813
import { bearerToken } from "../../../utils/auth";
914
import { logger } from "../../../../functions/server/logging";
1015

1116
import { PubSub } from "@google-cloud/pubsub";
1217
import { isValidBearer } from "../../../../../utils/hibp";
1318
import { config } from "../../../../../config";
14-
15-
const projectId = process.env.GCP_PUBSUB_PROJECT_ID;
16-
const topicName = process.env.GCP_PUBSUB_TOPIC_NAME;
17-
const subscriptionName = process.env.GCP_PUBSUB_SUBSCRIPTION_NAME;
19+
import {
20+
hibpNotifyRequestsTotal,
21+
incHibpNotifyFailure,
22+
} from "../../../../../instrumentation.node";
1823

1924
export type PostHibpNotificationRequestBody = {
2025
breachName: string;
@@ -29,23 +34,18 @@ export type PostHibpNotificationRequestBody = {
2934
* @param req
3035
*/
3136
export async function POST(req: NextRequest) {
37+
hibpNotifyRequestsTotal.inc();
38+
3239
let pubsub: PubSub;
3340
let json: PostHibpNotificationRequestBody;
34-
const hibpNotifyToken = process.env.HIBP_NOTIFY_TOKEN;
41+
const hibpNotifyToken = config.hibpNotifyToken;
42+
const projectId = config.gcp.projectId;
43+
const topicName = config.gcp.pubsub.hibpTopic;
3544
try {
36-
if (!projectId) {
37-
throw new Error("GCP_PUBSUB_PROJECT_ID env var not set");
38-
}
39-
if (!topicName) {
40-
throw new Error("GCP_PUBSUB_TOPIC_NAME env var not set");
41-
}
42-
if (!hibpNotifyToken) {
43-
throw new Error("HIBP_NOTIFY_TOKEN env var not set");
44-
}
45-
4645
const headerToken = bearerToken(req);
4746
if (!isValidBearer(headerToken, hibpNotifyToken)) {
4847
logger.error(`Received invalid header token: [${headerToken}]`);
48+
incHibpNotifyFailure("unauthorized");
4949
return NextResponse.json({ success: false }, { status: 401 });
5050
}
5151

@@ -55,44 +55,44 @@ export async function POST(req: NextRequest) {
5555
logger.error(
5656
"HIBP breach notification: requires breachName, hashPrefix, and hashSuffixes.",
5757
);
58+
incHibpNotifyFailure("bad-request");
5859
return NextResponse.json({ success: false }, { status: 400 });
5960
}
6061
} catch (ex) {
6162
logger.error("error_processing_breach_alert_request:", {
6263
exception: ex as string,
6364
});
65+
incHibpNotifyFailure("server-error");
6466
return NextResponse.json({ success: false }, { status: 500 });
6567
}
6668

6769
try {
6870
pubsub = new PubSub({ projectId });
6971
} catch (ex) {
7072
logger.error("error_connecting_to_pubsub:", { exception: ex as string });
71-
captureMessage(`error_connecting_to_pubsub: ${ex as string}`);
73+
incHibpNotifyFailure("pubsub-error");
7274
return NextResponse.json({ success: false }, { status: 429 });
7375
}
7476

7577
try {
7678
const topic = pubsub.topic(topicName);
79+
const [exists] = await topic.exists();
80+
if (!exists) {
81+
logger.error("error_connecting_to_pubsub: topic does not exist", {
82+
topic: topicName,
83+
});
84+
incHibpNotifyFailure("pubsub-error");
85+
return NextResponse.json({ success: false }, { status: 500 });
86+
}
7787
await topic.publishMessage({ json });
7888
logger.info("queued_breach_notification_success", {
7989
json,
8090
topic: topicName,
8191
});
8292
return NextResponse.json({ success: true }, { status: 200 });
8393
} catch {
84-
if (config.nodeEnv === "development") {
85-
if (!subscriptionName) {
86-
throw new Error("GCP_PUBSUB_SUBSCRIPTION_NAME env var not set");
87-
}
88-
await pubsub.createTopic(topicName);
89-
await pubsub.topic(topicName).createSubscription(subscriptionName);
90-
} else {
91-
logger.error("pubsub_topic_not_found:", { topicName });
92-
captureMessage(`pubsub_topic_not_found: ${topicName}`);
93-
return NextResponse.json({ success: false }, { status: 429 });
94-
}
9594
logger.error("error_queuing_hibp_breach:", { topicName });
95+
incHibpNotifyFailure("pubsub-error");
9696
return NextResponse.json({ success: false }, { status: 429 });
9797
}
9898
}

src/app/api/v1/metrics/route.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { NextResponse } from "next/server";
6+
import { registry } from "../../../../instrumentation.node";
7+
8+
export async function GET() {
9+
const metrics = await registry.metrics();
10+
return new NextResponse(metrics, {
11+
status: 200,
12+
headers: {
13+
"Content-Type":
14+
registry.contentType || "text/plain; version=0.0.4; charset=utf-8",
15+
},
16+
});
17+
}

src/config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
import "./app/functions/server/notInClientComponent";
77
import "./initializeEnvVars";
88

9+
// Don't need to have coverage on config object
10+
/* c8 ignore start */
11+
const isLocalOrTest =
12+
process.env.NODE_ENV === "test" || process.env.APP_ENV === "local";
13+
914
/**
1015
* Environment-specific values
1116
*
@@ -76,7 +81,19 @@ export const config = {
7681
fxRemoteSettingsWriterUser: process.env.FX_REMOTE_SETTINGS_WRITER_USER,
7782
fxRemoteSettingsWriterPass: process.env.FX_REMOTE_SETTINGS_WRITER_PASS,
7883
fxRemoteSettingsWriterServer: process.env.FX_REMOTE_SETTINGS_WRITER_SERVER,
84+
85+
gcp: {
86+
projectId: getEnvString("GCP_PUBSUB_PROJECT_ID", {
87+
fallbackValue: isLocalOrTest ? "your-project-name" : undefined,
88+
}),
89+
pubsub: {
90+
hibpTopic: getEnvString("GCP_PUBSUB_TOPIC_NAME", {
91+
fallbackValue: isLocalOrTest ? "hibp-breaches" : undefined,
92+
}),
93+
},
94+
},
7995
} as const;
96+
/* c8 ignore end */
8097

8198
/**
8299
* Like {@link getEnvString}, but also ensures the value is a valid integer

src/instrumentation.node.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
export const runtime = "nodejs";
6+
7+
import client from "prom-client";
8+
9+
type MetricsState = {
10+
registry: Readonly<client.Registry>;
11+
hibpNotifyRequestsTotal: client.Counter;
12+
hibpNotifyRequestFailuresTotal: client.Counter<"error">;
13+
};
14+
15+
declare global {
16+
var metrics: Readonly<MetricsState>;
17+
}
18+
19+
function getOrInitMetrics(): MetricsState {
20+
// Return cached state
21+
if (globalThis.metrics !== undefined) return globalThis.metrics;
22+
const registry = new client.Registry();
23+
client.collectDefaultMetrics({ register: registry });
24+
25+
/**
26+
* hibp_notify_requests_total
27+
* Metric to instrument the number of requests from HIBP notifying of breaches
28+
* Scope: "/hibp/notify"
29+
*/
30+
const hibpNotifyRequestsTotal = new client.Counter({
31+
name: "hibp_notify_requests_total",
32+
help: "Metric to instrument the number of requests from HIBP notifying of breaches",
33+
registers: [registry],
34+
});
35+
36+
/**
37+
* hibp_notify_request_failures_total{error="..."}
38+
* Metric to instrument the number of failed requests on HIBP notify endpoint
39+
* Labels:
40+
* - error: one of "timeout", "bad-request", "rate-limited"
41+
* Scope: "/hibp/notify"
42+
*/
43+
const hibpNotifyRequestFailuresTotal = new client.Counter<"error">({
44+
name: "hibp_notify_request_failures_total",
45+
help: "Metric to instrument the number of failed requests on HIBP notify endpoint",
46+
labelNames: ["error"],
47+
registers: [registry],
48+
});
49+
50+
const state: MetricsState = {
51+
registry,
52+
hibpNotifyRequestFailuresTotal,
53+
hibpNotifyRequestsTotal,
54+
};
55+
56+
// Make it readonly
57+
Object.defineProperty(globalThis, "metrics", {
58+
value: state,
59+
writable: false,
60+
configurable: false,
61+
enumerable: false,
62+
});
63+
return state;
64+
}
65+
66+
export const {
67+
registry,
68+
hibpNotifyRequestsTotal,
69+
hibpNotifyRequestFailuresTotal,
70+
} = getOrInitMetrics();
71+
72+
export type HibpNotifyFailureError =
73+
| "server-error"
74+
| "pubsub-error"
75+
| "bad-request"
76+
| "rate-limited"
77+
| "unauthorized"
78+
| "invalid-config";
79+
80+
/**
81+
* Increment helper to keep label values consistent
82+
* on hibpNotifyRequestFailuresTotal
83+
*/
84+
export function incHibpNotifyFailure(error: HibpNotifyFailureError, by = 1) {
85+
hibpNotifyRequestFailuresTotal.inc({ error }, by);
86+
}

src/instrumentation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as Sentry from "@sentry/nextjs";
66

7-
export function register() {
7+
export async function register() {
88
if (process.env.NEXT_RUNTIME === "nodejs") {
99
Sentry.init({
1010
environment: process.env.APP_ENV,
@@ -21,6 +21,7 @@ export function register() {
2121
// Setting this option to true will print useful information to the console while you're setting up Sentry.
2222
debug: false,
2323
});
24+
await import("./instrumentation.node");
2425
}
2526
}
2627

src/telemetry/prom_metrics.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# This documents custom metrics that are implemented in the app
2+
---
3+
version: 1.0
4+
5+
service: Monitor
6+
7+
hibp:
8+
hibp_notify_requests_total:
9+
description: |
10+
Metric to instrument the number of requests from HIBP notifying of breaches
11+
type: counter
12+
scope: |
13+
The "/hibp/notify" endpoint
14+
# No alerting
15+
alert_policy: []
16+
17+
hibp_notify_request_failures_total:
18+
description: |
19+
Metric to instrument the number of failed requests on HIBP notify endpoint
20+
type: counter
21+
labels:
22+
- name: error
23+
description: |
24+
Record the error. Any of "server-error", "pubsub-error", "bad-request",
25+
"rate-limited", "unauthorized", "invalid-config"
26+
scope: |
27+
The "/hibp/notify" endpoint
28+
alert_policy:
29+
- severity_level: S1
30+
trigger_condition: |
31+
Trigger when the aggregate failure rate is greater than
32+
20 req/sec.

0 commit comments

Comments
 (0)