[WEB-5413] feat: adding sentry error handling in web, space and admin (#8099)

This commit is contained in:
sriram veeraghanta
2025-11-12 19:03:47 +05:30
committed by GitHub
parent 0b78e03055
commit 30da349475
33 changed files with 872 additions and 414 deletions

View File

@@ -1,12 +1,12 @@
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000" VITE_API_BASE_URL="http://localhost:8000"
NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000" VITE_WEB_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001" VITE_ADMIN_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" VITE_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002" VITE_SPACE_BASE_URL="http://localhost:3002"
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" VITE_SPACE_BASE_PATH="/spaces"
NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100" VITE_LIVE_BASE_URL="http://localhost:3100"
NEXT_PUBLIC_LIVE_BASE_PATH="/live" VITE_LIVE_BASE_PATH="/live"

View File

@@ -28,35 +28,35 @@ FROM base AS installer
ENV NODE_ENV=production ENV NODE_ENV=production
# Public envs required at build time (pick up via process.env) # Public envs required at build time (pick up via process.env)
ARG NEXT_PUBLIC_API_BASE_URL="" ARG VITE_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ARG NEXT_PUBLIC_API_BASE_PATH="/api" ARG VITE_API_BASE_PATH="/api"
ENV NEXT_PUBLIC_API_BASE_PATH=$NEXT_PUBLIC_API_BASE_PATH ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH
ARG NEXT_PUBLIC_ADMIN_BASE_URL="" ARG VITE_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" ARG VITE_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL="" ARG VITE_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" ARG VITE_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
ARG NEXT_PUBLIC_LIVE_BASE_URL="" ARG VITE_LIVE_BASE_URL=""
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live" ARG VITE_LIVE_BASE_PATH="/live"
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL="" ARG VITE_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_PATH="" ARG VITE_WEB_BASE_PATH=""
ENV NEXT_PUBLIC_WEB_BASE_PATH=$NEXT_PUBLIC_WEB_BASE_PATH ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH
ARG NEXT_PUBLIC_WEBSITE_URL="https://plane.so" ARG VITE_WEBSITE_URL="https://plane.so"
ENV NEXT_PUBLIC_WEBSITE_URL=$NEXT_PUBLIC_WEBSITE_URL ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL
ARG NEXT_PUBLIC_SUPPORT_EMAIL="support@plane.so" ARG VITE_SUPPORT_EMAIL="support@plane.so"
ENV NEXT_PUBLIC_SUPPORT_EMAIL=$NEXT_PUBLIC_SUPPORT_EMAIL ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL
COPY .gitignore .gitignore COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/json/ .

View File

@@ -8,7 +8,7 @@ COPY . .
RUN corepack enable pnpm && pnpm add -g turbo RUN corepack enable pnpm && pnpm add -g turbo
RUN pnpm install RUN pnpm install
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" ENV VITE_ADMIN_BASE_PATH="/god-mode"
EXPOSE 3000 EXPOSE 3000

View File

@@ -0,0 +1,34 @@
/* eslint-disable import/order */
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
environment: process.env.VITE_SENTRY_ENVIRONMENT,
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
release: process.env.VITE_APP_VERSION,
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
: 0.1,
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
: 0.1,
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
: 0.1,
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
: 1.0,
integrations: [],
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});

View File

@@ -1,4 +1,5 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import * as Sentry from "@sentry/react-router";
import { Links, Meta, Outlet, Scripts } from "react-router"; import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router"; import type { LinksFunction } from "react-router";
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url"; import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
@@ -66,7 +67,11 @@ export function HydrateFallback() {
); );
} }
export function ErrorBoundary() { export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
return ( return (
<div> <div>
<p>Something went wrong.</p> <p>Something went wrong.</p>

View File

@@ -28,11 +28,10 @@
"@plane/ui": "workspace:*", "@plane/ui": "workspace:*",
"@plane/utils": "workspace:*", "@plane/utils": "workspace:*",
"@react-router/node": "^7.9.3", "@react-router/node": "^7.9.3",
"@sentry/react-router": "catalog:",
"@tanstack/react-virtual": "^3.13.12", "@tanstack/react-virtual": "^3.13.12",
"@tanstack/virtual-core": "^3.13.12", "@tanstack/virtual-core": "^3.13.12",
"@vercel/edge": "1.2.2",
"axios": "catalog:", "axios": "catalog:",
"dotenv": "^16.4.5",
"isbot": "^5.1.31", "isbot": "^5.1.31",
"lodash-es": "catalog:", "lodash-es": "catalog:",
"lucide-react": "catalog:", "lucide-react": "catalog:",

View File

@@ -1,7 +1,7 @@
import type { Config } from "@react-router/dev/config"; import type { Config } from "@react-router/dev/config";
import { joinUrlPath } from "@plane/utils"; import { joinUrlPath } from "@plane/utils";
const basePath = joinUrlPath(process.env.NEXT_PUBLIC_ADMIN_BASE_PATH ?? "", "/") ?? "/"; const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/";
export default { export default {
appDirectory: "app", appDirectory: "app",

View File

@@ -1,26 +1,23 @@
import path from "node:path"; import path from "node:path";
import { reactRouter } from "@react-router/dev/vite"; import { reactRouter } from "@react-router/dev/vite";
import dotenv from "dotenv";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import { joinUrlPath } from "@plane/utils"; import { joinUrlPath } from "@plane/utils";
dotenv.config({ path: path.resolve(__dirname, ".env") }); // Expose only vars starting with VITE_
const viteEnv = Object.keys(process.env)
// Automatically expose all environment variables prefixed with NEXT_PUBLIC_ .filter((k) => k.startsWith("VITE_"))
const publicEnv = Object.keys(process.env) .reduce<Record<string, string>>((a, k) => {
.filter((key) => key.startsWith("NEXT_PUBLIC_")) a[k] = process.env[k] ?? "";
.reduce<Record<string, string>>((acc, key) => { return a;
acc[key] = process.env[key] ?? "";
return acc;
}, {}); }, {});
const basePath = joinUrlPath(process.env.NEXT_PUBLIC_ADMIN_BASE_PATH ?? "", "/") ?? "/"; const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/";
export default defineConfig(() => ({ export default defineConfig(() => ({
base: basePath, base: basePath,
define: { define: {
"process.env": JSON.stringify(publicEnv), "process.env": JSON.stringify(viteEnv),
}, },
build: { build: {
assetsInlineLimit: 0, assetsInlineLimit: 0,

View File

@@ -1,12 +1,12 @@
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000" VITE_API_BASE_URL="http://localhost:8000"
NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000" VITE_WEB_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001" VITE_ADMIN_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" VITE_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002" VITE_SPACE_BASE_URL="http://localhost:3002"
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" VITE_SPACE_BASE_PATH="/spaces"
NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100" VITE_LIVE_BASE_URL="http://localhost:3100"
NEXT_PUBLIC_LIVE_BASE_PATH="/live" VITE_LIVE_BASE_PATH="/live"

View File

@@ -12,7 +12,7 @@ RUN pnpm install
EXPOSE 3002 EXPOSE 3002
ENV NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" ENV VITE_SPACE_BASE_PATH="/spaces"
VOLUME [ "/app/node_modules", "/app/apps/space/node_modules"] VOLUME [ "/app/node_modules", "/app/apps/space/node_modules"]

View File

@@ -28,35 +28,36 @@ FROM base AS installer
ENV NODE_ENV=production ENV NODE_ENV=production
# Public envs required at build time (pick up via process.env) # Public envs required at build time (pick up via process.env)
ARG NEXT_PUBLIC_API_BASE_URL="" ARG VITE_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ARG NEXT_PUBLIC_API_BASE_PATH="/api" ARG VITE_API_BASE_PATH="/api"
ENV NEXT_PUBLIC_API_BASE_PATH=$NEXT_PUBLIC_API_BASE_PATH ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH
ARG NEXT_PUBLIC_ADMIN_BASE_URL="" ARG VITE_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" ARG VITE_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL="" ARG VITE_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" ARG VITE_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
ARG NEXT_PUBLIC_LIVE_BASE_URL="" ARG VITE_LIVE_BASE_URL=""
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live" ARG VITE_LIVE_BASE_PATH="/live"
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL="" ARG VITE_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_PATH="" ARG VITE_WEB_BASE_PATH=""
ENV NEXT_PUBLIC_WEB_BASE_PATH=$NEXT_PUBLIC_WEB_BASE_PATH ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH
ARG NEXT_PUBLIC_WEBSITE_URL="https://plane.so" ARG VITE_WEBSITE_URL="https://plane.so"
ENV NEXT_PUBLIC_WEBSITE_URL=$NEXT_PUBLIC_WEBSITE_URL ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL
ARG NEXT_PUBLIC_SUPPORT_EMAIL="support@plane.so"
ENV NEXT_PUBLIC_SUPPORT_EMAIL=$NEXT_PUBLIC_SUPPORT_EMAIL ARG VITE_SUPPORT_EMAIL="support@plane.so"
ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL
COPY .gitignore .gitignore COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/json/ .

View File

@@ -0,0 +1,34 @@
/* eslint-disable import/order */
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
environment: process.env.VITE_SENTRY_ENVIRONMENT,
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
release: process.env.VITE_APP_VERSION,
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
: 0.1,
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
: 0.1,
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
: 0.1,
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
: 1.0,
integrations: [],
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});

View File

@@ -33,7 +33,7 @@ export async function loader({ params }: Route.LoaderArgs) {
} }
try { try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/anchor/${anchor}/meta/`); const response = await fetch(`${process.env.VITE_API_BASE_URL}/api/public/anchor/${anchor}/meta/`);
if (!response.ok) { if (!response.ok) {
return { metadata: null }; return { metadata: null };

View File

@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/react-router";
import { Links, Meta, Outlet, Scripts } from "react-router"; import { Links, Meta, Outlet, Scripts } from "react-router";
import type { HeadersFunction, LinksFunction } from "react-router"; import type { HeadersFunction, LinksFunction } from "react-router";
// assets // assets
@@ -79,6 +80,10 @@ export function HydrateFallback() {
); );
} }
export function ErrorBoundary() { export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
return <ErrorPage />; return <ErrorPage />;
} }

View File

@@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
// helpers // helpers
import { SUPPORT_EMAIL } from "./common.helper"; import { SUPPORT_EMAIL } from "@plane/constants";
export enum EPageTypes { export enum EPageTypes {
INIT = "INIT", INIT = "INIT",

View File

@@ -1,4 +1,2 @@
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "";
export const resolveGeneralTheme = (resolvedTheme: string | undefined) => export const resolveGeneralTheme = (resolvedTheme: string | undefined) =>
resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system"; resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system";

View File

@@ -32,10 +32,10 @@
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@react-router/node": "^7.9.3", "@react-router/node": "^7.9.3",
"@react-router/serve": "^7.9.5", "@react-router/serve": "^7.9.5",
"@sentry/react-router": "catalog:",
"axios": "catalog:", "axios": "catalog:",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"isbot": "^5.1.31", "isbot": "^5.1.31",
"lodash-es": "catalog:", "lodash-es": "catalog:",
"lucide-react": "catalog:", "lucide-react": "catalog:",

View File

@@ -1,7 +1,7 @@
import type { Config } from "@react-router/dev/config"; import type { Config } from "@react-router/dev/config";
import { joinUrlPath } from "@plane/utils"; import { joinUrlPath } from "@plane/utils";
const basePath = joinUrlPath(process.env.NEXT_PUBLIC_SPACE_BASE_PATH ?? "", "/") ?? "/"; const basePath = joinUrlPath(process.env.VITE_SPACE_BASE_PATH ?? "", "/") ?? "/";
export default { export default {
appDirectory: "app", appDirectory: "app",

View File

@@ -1,26 +1,23 @@
import path from "node:path"; import path from "node:path";
import { reactRouter } from "@react-router/dev/vite"; import { reactRouter } from "@react-router/dev/vite";
import dotenv from "dotenv";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import { joinUrlPath } from "@plane/utils"; import { joinUrlPath } from "@plane/utils";
dotenv.config({ path: path.resolve(__dirname, ".env") }); // Expose only vars starting with VITE_
const viteEnv = Object.keys(process.env)
// Automatically expose all environment variables prefixed with NEXT_PUBLIC_ .filter((k) => k.startsWith("VITE_"))
const publicEnv = Object.keys(process.env) .reduce<Record<string, string>>((a, k) => {
.filter((key) => key.startsWith("NEXT_PUBLIC_")) a[k] = process.env[k] ?? "";
.reduce<Record<string, string>>((acc, key) => { return a;
acc[key] = process.env[key] ?? "";
return acc;
}, {}); }, {});
const basePath = joinUrlPath(process.env.NEXT_PUBLIC_SPACE_BASE_PATH ?? "", "/") ?? "/"; const basePath = joinUrlPath(process.env.VITE_SPACE_BASE_PATH ?? "", "/") ?? "/";
export default defineConfig(() => ({ export default defineConfig(() => ({
base: basePath, base: basePath,
define: { define: {
"process.env": JSON.stringify(publicEnv), "process.env": JSON.stringify(viteEnv),
}, },
build: { build: {
assetsInlineLimit: 0, assetsInlineLimit: 0,

View File

@@ -1,12 +1,12 @@
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000" VITE_API_BASE_URL="http://localhost:8000"
NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000" VITE_WEB_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001" VITE_ADMIN_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" VITE_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002" VITE_SPACE_BASE_URL="http://localhost:3002"
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" VITE_SPACE_BASE_PATH="/spaces"
NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100" VITE_LIVE_BASE_URL="http://localhost:3100"
NEXT_PUBLIC_LIVE_BASE_PATH="/live" VITE_LIVE_BASE_PATH="/live"

View File

@@ -41,29 +41,29 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json COPY turbo.json turbo.json
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
ARG NEXT_PUBLIC_API_BASE_URL="" ARG VITE_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL="" ARG VITE_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" ARG VITE_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_LIVE_BASE_URL="" ARG VITE_LIVE_BASE_URL=""
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live" ARG VITE_LIVE_BASE_PATH="/live"
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL="" ARG VITE_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" ARG VITE_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL="" ARG VITE_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV TURBO_TELEMETRY_DISABLED=1 ENV TURBO_TELEMETRY_DISABLED=1

View File

@@ -9,11 +9,11 @@
// }; // };
// const urls = [ // const urls = [
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/instances/`, // `${process.env.VITE_API_BASE_URL}/api/instances/`,
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/`, // `${process.env.VITE_API_BASE_URL}/api/users/me/`,
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/profile/`, // `${process.env.VITE_API_BASE_URL}/api/users/me/profile/`,
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/settings/`, // `${process.env.VITE_API_BASE_URL}/api/users/me/settings/`,
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/workspaces/?v=${Date.now()}`, // `${process.env.VITE_API_BASE_URL}/api/users/me/workspaces/?v=${Date.now()}`,
// ]; // ];
// urls.forEach((url) => preloadItem(url)); // urls.forEach((url) => preloadItem(url));

View File

@@ -0,0 +1,34 @@
/* eslint-disable import/order */
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
environment: process.env.VITE_SENTRY_ENVIRONMENT,
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
release: process.env.VITE_APP_VERSION,
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
: 0.1,
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
: 0.1,
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
: 0.1,
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
: 1.0,
integrations: [],
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});

View File

@@ -50,7 +50,7 @@ export const meta = () => [
]; ];
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
const isSessionRecorderEnabled = parseInt(process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0"); const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
return ( return (
<html lang="en"> <html lang="en">
@@ -86,16 +86,16 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</div> </div>
</AppProvider> </AppProvider>
</body> </body>
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && ( {process.env.VITE_PLAUSIBLE_DOMAIN && (
<Script defer data-domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN} src="https://plausible.io/js/script.js" /> <Script defer data-domain={process.env.VITE_PLAUSIBLE_DOMAIN} src="https://plausible.io/js/script.js" />
)} )}
{!!isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && ( {!!isSessionRecorderEnabled && process.env.VITE_SESSION_RECORDER_KEY && (
<Script id="clarity-tracking"> <Script id="clarity-tracking">
{`(function(c,l,a,r,i,t,y){ {`(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];if(y){y.parentNode.insertBefore(t,y);} y=l.getElementsByTagName(r)[0];if(y){y.parentNode.insertBefore(t,y);}
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY}");`} })(window, document, "clarity", "script", "${process.env.VITE_SESSION_RECORDER_KEY}");`}
</Script> </Script>
)} )}
</html> </html>

View File

@@ -1,4 +1,5 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import * as Sentry from "@sentry/react-router";
import Script from "next/script"; import Script from "next/script";
import { Links, Meta, Outlet, Scripts } from "react-router"; import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router"; import type { LinksFunction } from "react-router";
@@ -35,7 +36,7 @@ export const links: LinksFunction = () => [
]; ];
export function Layout({ children }: { children: ReactNode }) { export function Layout({ children }: { children: ReactNode }) {
const isSessionRecorderEnabled = parseInt(process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0"); const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
return ( return (
<html lang="en"> <html lang="en">
@@ -67,20 +68,16 @@ export function Layout({ children }: { children: ReactNode }) {
</div> </div>
</AppProvider> </AppProvider>
<Scripts /> <Scripts />
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && ( {process.env.VITE_PLAUSIBLE_DOMAIN && (
<Script <Script defer data-domain={process.env.VITE_PLAUSIBLE_DOMAIN} src="https://plausible.io/js/script.js" />
defer
data-domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
src="https://plausible.io/js/script.js"
/>
)} )}
{!!isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && ( {!!isSessionRecorderEnabled && process.env.VITE_SESSION_RECORDER_KEY && (
<Script id="clarity-tracking"> <Script id="clarity-tracking">
{`(function(c,l,a,r,i,t,y){ {`(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];if(y){y.parentNode.insertBefore(t,y);} y=l.getElementsByTagName(r)[0];if(y){y.parentNode.insertBefore(t,y);}
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY}");`} })(window, document, "clarity", "script", "${process.env.VITE_SESSION_RECORDER_KEY}");`}
</Script> </Script>
)} )}
</body> </body>
@@ -127,5 +124,9 @@ export function HydrateFallback() {
} }
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error) {
Sentry.captureException(error);
}
return <CustomErrorComponent error={error} />; return <CustomErrorComponent error={error} />;
} }

View File

@@ -30,7 +30,7 @@ export const SelectRepository: React.FC<Props> = (props) => {
const getKey = (pageIndex: number) => { const getKey = (pageIndex: number) => {
if (!workspaceSlug || !integration) return; if (!workspaceSlug || !integration) return;
return `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-integrations/${ return `${process.env.VITE_API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-integrations/${
integration.id integration.id
}/github-repositories/?page=${++pageIndex}`; }/github-repositories/?page=${++pageIndex}`;
}; };

View File

@@ -38,8 +38,7 @@ const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
); );
const currentWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug?.toString()); const currentWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug?.toString());
const is_telemetry_enabled = instance?.is_telemetry_enabled || false; const is_telemetry_enabled = instance?.is_telemetry_enabled || false;
const is_posthog_enabled = const is_posthog_enabled = process.env.VITE_POSTHOG_KEY && process.env.VITE_POSTHOG_HOST && is_telemetry_enabled;
process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && is_telemetry_enabled;
useEffect(() => { useEffect(() => {
if (user && hydrated) { if (user && hydrated) {
@@ -62,9 +61,9 @@ const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
}, [user, currentProjectRole, currentWorkspaceRole, currentWorkspace, hydrated]); }, [user, currentProjectRole, currentWorkspaceRole, currentWorkspace, hydrated]);
useEffect(() => { useEffect(() => {
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY; const posthogKey = process.env.VITE_POSTHOG_KEY;
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST; const posthogHost = process.env.VITE_POSTHOG_HOST;
const isDebugMode = process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "1"; const isDebugMode = process.env.VITE_POSTHOG_DEBUG === "1";
if (posthogKey && posthogHost) { if (posthogKey && posthogHost) {
posthog.init(posthogKey, { posthog.init(posthogKey, {
api_host: posthogHost, api_host: posthogHost,

View File

@@ -37,13 +37,13 @@
"@posthog/react": "^1.4.0", "@posthog/react": "^1.4.0",
"@react-pdf/renderer": "^3.4.5", "@react-pdf/renderer": "^3.4.5",
"@react-router/node": "^7.9.3", "@react-router/node": "^7.9.3",
"@sentry/react-router": "catalog:",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"axios": "catalog:", "axios": "catalog:",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"comlink": "^4.4.1", "comlink": "^4.4.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"emoji-picker-react": "^4.5.16", "emoji-picker-react": "^4.5.16",
"export-to-csv": "^1.4.0", "export-to-csv": "^1.4.0",
"isbot": "^5.1.31", "isbot": "^5.1.31",

View File

@@ -1,22 +1,19 @@
import path from "node:path"; import path from "node:path";
import { reactRouter } from "@react-router/dev/vite"; import { reactRouter } from "@react-router/dev/vite";
import dotenv from "dotenv";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
dotenv.config({ path: path.resolve(__dirname, ".env") }); // Expose only vars starting with VITE_
const viteEnv = Object.keys(process.env)
// Automatically expose all environment variables prefixed with NEXT_PUBLIC_ .filter((k) => k.startsWith("VITE_"))
const publicEnv = Object.keys(process.env) .reduce<Record<string, string>>((a, k) => {
.filter((key) => key.startsWith("NEXT_PUBLIC_")) a[k] = process.env[k] ?? "";
.reduce<Record<string, string>>((acc, key) => { return a;
acc[key] = process.env[key] ?? "";
return acc;
}, {}); }, {});
export default defineConfig(() => ({ export default defineConfig(() => ({
define: { define: {
"process.env": JSON.stringify(publicEnv), "process.env": JSON.stringify(viteEnv),
}, },
build: { build: {
assetsInlineLimit: 0, assetsInlineLimit: 0,

View File

@@ -1,26 +1,26 @@
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || ""; export const API_BASE_URL = process.env.VITE_API_BASE_URL || "";
export const API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH || ""; export const API_BASE_PATH = process.env.VITE_API_BASE_PATH || "";
export const API_URL = encodeURI(`${API_BASE_URL}${API_BASE_PATH}`); export const API_URL = encodeURI(`${API_BASE_URL}${API_BASE_PATH}`);
// God Mode Admin App Base Url // God Mode Admin App Base Url
export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || ""; export const ADMIN_BASE_URL = process.env.VITE_ADMIN_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || ""; export const ADMIN_BASE_PATH = process.env.VITE_ADMIN_BASE_PATH || "";
export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`); export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`);
// Publish App Base Url // Publish App Base Url
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || ""; export const SPACE_BASE_URL = process.env.VITE_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || ""; export const SPACE_BASE_PATH = process.env.VITE_SPACE_BASE_PATH || "";
export const SITES_URL = encodeURI(`${SPACE_BASE_URL}${SPACE_BASE_PATH}`); export const SITES_URL = encodeURI(`${SPACE_BASE_URL}${SPACE_BASE_PATH}`);
// Live App Base Url // Live App Base Url
export const LIVE_BASE_URL = process.env.NEXT_PUBLIC_LIVE_BASE_URL || ""; export const LIVE_BASE_URL = process.env.VITE_LIVE_BASE_URL || "";
export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || ""; export const LIVE_BASE_PATH = process.env.VITE_LIVE_BASE_PATH || "";
export const LIVE_URL = encodeURI(`${LIVE_BASE_URL}${LIVE_BASE_PATH}`); export const LIVE_URL = encodeURI(`${LIVE_BASE_URL}${LIVE_BASE_PATH}`);
// Web App Base Url // Web App Base Url
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || ""; export const WEB_BASE_URL = process.env.VITE_WEB_BASE_URL || "";
export const WEB_BASE_PATH = process.env.NEXT_PUBLIC_WEB_BASE_PATH || ""; export const WEB_BASE_PATH = process.env.VITE_WEB_BASE_PATH || "";
export const WEB_URL = encodeURI(`${WEB_BASE_URL}${WEB_BASE_PATH}`); export const WEB_URL = encodeURI(`${WEB_BASE_URL}${WEB_BASE_PATH}`);
// plane website url // plane website url
export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL || "https://plane.so"; export const WEBSITE_URL = process.env.VITE_WEBSITE_URL || "https://plane.so";
// support email // support email
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@plane.so"; export const SUPPORT_EMAIL = process.env.VITE_SUPPORT_EMAIL || "support@plane.so";
// marketing links // marketing links
export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing"; export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing";
export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact"; export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact";

806
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,9 @@ catalog:
"@atlaskit/pragmatic-drag-and-drop-hitbox": 1.1.0 "@atlaskit/pragmatic-drag-and-drop-hitbox": 1.1.0
"@atlaskit/pragmatic-drag-and-drop": 1.7.4 "@atlaskit/pragmatic-drag-and-drop": 1.7.4
"@bprogress/core": ^1.3.4 "@bprogress/core": ^1.3.4
"@sentry/node": 10.5.0 "@sentry/node": 10.24.0
"@sentry/profiling-node": 10.5.0 "@sentry/profiling-node": 10.24.0
"@sentry/react-router": 10.24.0
axios: 1.12.0 axios: 1.12.0
mobx: 6.12.0 mobx: 6.12.0
mobx-react: 9.1.1 mobx-react: 9.1.1

View File

@@ -2,22 +2,22 @@
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"globalEnv": [ "globalEnv": [
"NODE_ENV", "NODE_ENV",
"NEXT_PUBLIC_API_BASE_URL", "VITE_API_BASE_URL",
"NEXT_PUBLIC_ADMIN_BASE_URL", "VITE_ADMIN_BASE_URL",
"NEXT_PUBLIC_ADMIN_BASE_PATH", "VITE_ADMIN_BASE_PATH",
"NEXT_PUBLIC_SPACE_BASE_URL", "VITE_SPACE_BASE_URL",
"NEXT_PUBLIC_SPACE_BASE_PATH", "VITE_SPACE_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL", "VITE_WEB_BASE_URL",
"NEXT_PUBLIC_LIVE_BASE_URL", "VITE_LIVE_BASE_URL",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN", "VITE_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_CRISP_ID", "VITE_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER", "VITE_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_SESSION_RECORDER_KEY", "VITE_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS", "VITE_EXTRA_IMAGE_DOMAINS",
"NEXT_PUBLIC_POSTHOG_KEY", "VITE_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST", "VITE_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG", "VITE_POSTHOG_DEBUG",
"NEXT_PUBLIC_SUPPORT_EMAIL", "VITE_SUPPORT_EMAIL",
"ENABLE_EXPERIMENTAL_COREPACK" "ENABLE_EXPERIMENTAL_COREPACK"
], ],
"globalDependencies": ["pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc"], "globalDependencies": ["pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc"],