Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/release-debug-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Release Debug UI Image

on:
push:
branches: [main]
paths: [pyproject.toml]
workflow_dispatch:
inputs:
ref:
description: Release tag or commit to build
required: true
type: string

permissions:
contents: read

jobs:
debug-ui:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Read release version
id: release
run: |
python - <<'PY'
import os
import subprocess
import tomllib

with open("pyproject.toml", "rb") as file:
version = tomllib.load(file)["tool"]["poetry"]["version"]
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
print(f"version={version}", file=output)
print(f"commit={commit}", file=output)
print(f"image=ghcr.io/{os.environ['GITHUB_REPOSITORY_OWNER'].lower()}/keep-frontend", file=output)
PY

- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and publish UI with browser source maps
uses: docker/build-push-action@v6
with:
context: keep-ui
file: docker/Dockerfile.ui
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.release.outputs.image }}:${{ steps.release.outputs.version }}-dev
build-args: |
KEEP_INCLUDE_SOURCES=true
KEEP_VERSION=${{ steps.release.outputs.version }}-dev
GIT_COMMIT_HASH=${{ steps.release.outputs.commit }}
cache-from: type=gha,scope=debug-ui
cache-to: type=gha,mode=max,scope=debug-ui
2 changes: 2 additions & 0 deletions docker/Dockerfile.ui
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ RUN npm ci --noproxy registry.npmjs.org --maxsockets 1

# Rebuild the source code only when needed
FROM base AS builder
ARG KEEP_INCLUDE_SOURCES=false
ENV KEEP_INCLUDE_SOURCES=${KEEP_INCLUDE_SOURCES}
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
Expand Down
29 changes: 28 additions & 1 deletion docs/deployment/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ The docker-compose.yml contains 3 services:
- [keep-frontend](https://console.cloud.google.com/artifacts/docker/keephq/us-central1/keep/keep-ui?project=keephq) - a nextjs app that serves as Keep UI interface.
- [keep-websocket-server](https://docs.soketi.app/getting-started/installation/docker) - Soketi (a pusher compatible websocket server) for real time alerting.

### Debugging Keep UI

The debug UI image uses the same production server as the regular image, with
browser source maps and console output retained. The `Release Debug UI Image`
workflow publishes `ghcr.io/keephq/keep-frontend:<version>-dev` when the release
version changes on `main`. It can also be run manually with a release tag or
commit containing debug-image support.

To debug an existing deployment, replace only the frontend image with the
matching `<version>-dev` image. Keep its environment variables, ports, and
oauth2-proxy configuration, then reproduce the error with browser developer
tools open and JavaScript source maps enabled. This also works in Kubernetes
without running a local development server.

You can build the image yourself from the corresponding release checkout:

```sh
docker build -f docker/Dockerfile.ui \
--build-arg KEEP_INCLUDE_SOURCES=true \
-t keep-ui:debug keep-ui
```

`KEEP_INCLUDE_SOURCES` is a **build argument**. Setting it only in a running
container cannot generate source maps or change the routes compiled by Next.js.
Source maps are included even when Sentry is disabled at build time. Debug images
serve frontend source code to browsers; switch back to the regular image after
debugging. Regular builds continue to block requests for source maps.

### Reinstall Keep with the option to refresh from scratch

`Caution:` This usage context will refresh from the beginning and Keep's data and settings will be erased. Even other containers on this host are also erased. So please consider when using the steps below.
Expand Down Expand Up @@ -64,4 +92,3 @@ rm -rf state/*
```
docker compose up -d
```

2 changes: 1 addition & 1 deletion docs/snippets/providers/graylog-snippet-autogenerated.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ To send alerts from Graylog to Keep, Use the following webhook url to configure
- **Headers**: X-API-KEY:{api_key}
4. Erase the Body Template.
5. Click on "Create Notification".
6. Go the the `Event Definitions` tab, and select the Event Definition that will trigger the alert you want to send to Keep and click on More > Edit.
6. Go to the `Event Definitions` tab, and select the Event Definition that will trigger the alert you want to send to Keep and click on More > Edit.
7. Go to "Notifications" tab.
8. Click on "Add Notification" and select the "keep-graylog-webhook-integration" that you created in step 3.
9. Click on "Add Notification".
Expand Down
73 changes: 73 additions & 0 deletions keep-ui/__tests__/next-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { withSentryConfig } from "@sentry/nextjs";

jest.mock("@sentry/nextjs", () => ({
withSentryConfig: jest.fn((config) => config),
}));

describe("production source maps", () => {
const originalEnv = process.env;

afterEach(() => {
process.env = originalEnv;
jest.clearAllMocks();
});

it.each(["true", "false"])(
"serves debug source maps with SENTRY_DISABLED=%s",
async (sentryDisabled) => {
process.env = {
...originalEnv,
NODE_ENV: "production",
KEEP_INCLUDE_SOURCES: "true",
SENTRY_DISABLED: sentryDisabled,
};
let config: any;
jest.isolateModules(() => {
config = require("../next.config");
});

expect(config.productionBrowserSourceMaps).toBe(true);
expect(config.compiler.removeConsole).toBe(false);
expect(await config.rewrites()).toEqual([]);
if (sentryDisabled === "false") {
expect(withSentryConfig).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
sourcemaps: { deleteSourcemapsAfterUpload: false },
})
);
}
}
);

it.each([undefined, "false"])(
"blocks production source maps with KEEP_INCLUDE_SOURCES=%s",
async (includeSources) => {
process.env = {
...originalEnv,
NODE_ENV: "production",
SENTRY_DISABLED: "false",
};
if (includeSources === undefined) {
delete process.env.KEEP_INCLUDE_SOURCES;
} else {
process.env.KEEP_INCLUDE_SOURCES = includeSources;
}
let config: any;
jest.isolateModules(() => {
config = require("../next.config");
});

expect(config.compiler.removeConsole).toBe(true);
expect(await config.rewrites()).toEqual({
beforeFiles: [{ source: "/:path*.map", destination: "/404" }],
});
expect(withSentryConfig).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
sourcemaps: { deleteSourcemapsAfterUpload: true },
})
);
}
);
});
23 changes: 23 additions & 0 deletions keep-ui/app/__tests__/global-error.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { fireEvent, render, screen } from "@testing-library/react";
import * as Sentry from "@sentry/nextjs";
import GlobalError from "../global-error";

jest.unmock("next-auth/react");
jest.unmock("@/shared/lib/hooks/useApi");
jest.unmock("@/utils/hooks/useConfig");
jest.mock("@sentry/nextjs", () => ({ captureException: jest.fn() }));

it("renders the original error and retries without layout providers", () => {
const error = Object.assign(new Error("Unable to load the session"), {
digest: "error-reference",
});
const reset = jest.fn();

render(<GlobalError error={error} reset={reset} />, { container: document });

expect(screen.getByText(error.message)).toBeInTheDocument();
expect(screen.getByText("Error reference: error-reference")).toBeInTheDocument();
expect(Sentry.captureException).toHaveBeenCalledWith(error);
fireEvent.click(screen.getByRole("button", { name: "Try again" }));
expect(reset).toHaveBeenCalledTimes(1);
});
20 changes: 17 additions & 3 deletions keep-ui/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
"use client";

import { ErrorComponent } from "@/shared/ui";
import { useEffect } from "react";
import * as Sentry from "@sentry/nextjs";

export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);

// This boundary replaces the root layout, including all of its providers.
// Keep it independent of session, configuration, and API hooks.
return (
<html>
<html lang="en">
<body>
<ErrorComponent error={error} />
<main>
<h1>Something went wrong</h1>
<p>{error.message || "An unexpected error occurred."}</p>
{error.digest && <p>Error reference: {error.digest}</p>}
<button onClick={reset}>Try again</button>
</main>
</body>
</html>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,9 @@ describe("AlertPresetManager", () => {
/>
);

// The test alerts button is rendered, check by its color and variant
const buttons = screen.getAllByRole("button");
const testButton = buttons.find(button =>
button.className.includes("border-orange-500") &&
button.className.includes("text-orange-500")
);
expect(testButton).toBeInTheDocument();
const testButton = screen.getByRole("button", { name: "Test alerts" });
fireEvent.click(testButton);
expect(screen.getByTestId("push-alert-modal")).toBeInTheDocument();
});

it("should maintain button order and spacing", () => {
Expand Down Expand Up @@ -139,4 +135,4 @@ describe("AlertPresetManager", () => {
});


});
});
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ export function AlertPresetManager({
<Button
variant="secondary"
tooltip="Test alerts"
aria-label="Test alerts"
data-testid="test-alerts-button"
size="sm"
icon={GrTest}
onClick={handleAddAlertModalOpen}
Expand Down
13 changes: 6 additions & 7 deletions keep-ui/next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { withSentryConfig } = require("@sentry/nextjs");
const includeSources = process.env.KEEP_INCLUDE_SOURCES === "true";

const isSentryDisabled =
process.env.SENTRY_DISABLED === "true" ||
Expand Down Expand Up @@ -108,10 +109,10 @@ const nextConfig = {
],
},
compiler: {
removeConsole: process.env.NODE_ENV === "production",
removeConsole: process.env.NODE_ENV === "production" && !includeSources,
},
output: "standalone",
productionBrowserSourceMaps: !isSentryDisabled,
productionBrowserSourceMaps: includeSources || !isSentryDisabled,
async redirects() {
const workflowRawYamlRedirects = [
{
Expand Down Expand Up @@ -158,7 +159,7 @@ const nextConfig = {
process.env.VERCEL_ENV === "production" ||
process.env.NODE_ENV === "production";

if (isVercelProdDeploy) {
if (isVercelProdDeploy && !includeSources) {
return {
beforeFiles: [
{
Expand Down Expand Up @@ -197,10 +198,8 @@ const sentryConfig = {
// side errors will fail.
tunnelRoute: "/monitoring",

// Hides source maps from generated client bundles
hideSourceMaps: true,
sourceMaps: {
deleteSourcemapsAfterUpload: process.env.KEEP_INCLUDE_SOURCES !== "true",
sourcemaps: {
deleteSourcemapsAfterUpload: !includeSources,
},

// Automatically tree-shake Sentry logger statements to reduce bundle size
Expand Down
Loading
Loading