All files / server/src/features/sentry-tunnel sentryTunnelService.ts

100% Statements 35/35
92.85% Branches 13/14
100% Functions 4/4
100% Lines 35/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122                  43x 43x 43x                   43x         22x 22x             7x                 15x   11x 11x 11x 11x                   4x     43x     13x 13x   13x     12x   12x 1x 1x             11x   11x 1x 1x             10x       10x   22x 17x     10x 6x 6x             4x   1x 1x              
import url from "node:url";
import { ApiError } from "shared/types/api/errors";
import { logger } from "server/utils/logger";
import {
  flattenErrorMessageChain,
  retryWithDelays,
  sentryRetryDelaysMs,
} from "server/utils/sentryRetry";
 
const sentryHost = "sentry.io";
const knownProjectIds = new Set(["/6579203", "/6578391", "/6579491"]);
const fetchTimeoutMs = 1000 * 15;
 
interface ResendSentryError extends ApiError {
  errorId: "unknown";
}
 
type ForwardAttempt =
  | { delivered: true }
  | { delivered: false; retryable: boolean; error: Error };
 
const attemptForward = async (
  sentryUrl: string,
  envelope: Buffer,
): Promise<ForwardAttempt> => {
  let response: Response;
  try {
    response = await fetch(sentryUrl, {
      method: "POST",
      headers: { "Content-Type": "application/x-sentry-envelope" },
      body: new Uint8Array(envelope),
      signal: AbortSignal.timeout(fetchTimeoutMs),
    });
  } catch (fetchError) {
    return {
      delivered: false,
      retryable: true,
      error: new Error(
        `Sentry tunnel: upstream request failed: ${flattenErrorMessageChain(fetchError)}`,
      ),
    };
  }
 
  if (!response.ok) {
    // Sentry explains rejections in the x-sentry-error header and/or body
    const sentryError = response.headers.get("x-sentry-error");
    const body = (await response.text()).slice(0, 500);
    const details = [sentryError, body].filter(Boolean).join(" / ");
    return {
      delivered: false,
      // Retrying a 4xx would resend an envelope Sentry already rejected
      retryable: response.status >= 500,
      error: new Error(
        `Sentry tunnel: upstream responded with ${response.status}${details ? `: ${details}` : ""}`,
      ),
    };
  }
 
  return { delivered: true };
};
 
export const resendSentryRequest = async (
  envelope: Buffer,
): Promise<null | ResendSentryError> => {
  try {
    const piece = envelope.subarray(0, envelope.indexOf("\n"));
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const header = JSON.parse(piece.toString());
 
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    const { hostname, pathname } = new url.URL(header.dsn as string);
 
    if (!hostname.endsWith(`.${sentryHost}`)) {
      logger.error(new Error(`invalid host: ${hostname}`));
      return {
        message: "Sentry tunnel: Invalid host",
        status: "error",
        errorId: "unknown",
      };
    }
 
    const projectId = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
 
    if (!knownProjectIds.has(projectId)) {
      logger.error(new Error(`invalid project id: ${projectId}`));
      return {
        message: "Sentry tunnel: Invalid project",
        status: "error",
        errorId: "unknown",
      };
    }
 
    const sentryUrl = `https://${hostname}/api${projectId}/envelope/`;
 
    // Upstream failures are usually transient load shedding, so retry with
    // increasing delays before logging the failure
    const attempt = await retryWithDelays(
      sentryRetryDelaysMs,
      async () => attemptForward(sentryUrl, envelope),
      (result) => !result.delivered && result.retryable,
    );
 
    if (!attempt.delivered) {
      logger.error(attempt.error);
      return {
        message: "Sentry tunnel: Upstream error",
        status: "error",
        errorId: "unknown",
      };
    }
 
    return null;
  } catch (error) {
    logger.error(new Error("Sentry tunnel error", { cause: error }));
    return {
      message: "Sentry tunnel: Unknown error",
      status: "error",
      errorId: "unknown",
    };
  }
};