Compare commits

...

7 Commits

Author SHA1 Message Date
Nawaz Dhandala
3a445eabd9 refactor(session): remove TokenRefresher and session refresh integration from APIs 2025-11-10 12:56:02 +00:00
Nawaz Dhandala
645ebdbb4a feat(session): add TokenRefresher and wire automatic access_token refresh into BaseAPI and StatusPage API 2025-11-10 12:39:13 +00:00
Nawaz Dhandala
3df2152737 chore(migration): add migration to create UserSession and StatusPagePrivateUserSession tables and register it in migrations index 2025-11-10 12:17:38 +00:00
Nawaz Dhandala
65758b9eae chore(session): add missing @Index decorators to session models (UserSession, StatusPagePrivateUserSession) for userId, refreshToken, projectId, statusPageId, and statusPagePrivateUserId 2025-11-10 12:11:07 +00:00
Nawaz Dhandala
64b4a309d4 chore(session): remove unused HashedString import from UserSession.ts 2025-11-10 11:17:32 +00:00
Nawaz Dhandala
e833e07a01 refactor(session): replace HashedString with LongText for refreshToken columns
Remove HashedString import and DB transformer; change ColumnType/Length to LongText
and store refreshToken as plain string in UserSession and StatusPagePrivateUserSession.
Update table column metadata accordingly.
2025-11-10 11:14:45 +00:00
Nawaz Dhandala
8844a5e302 feat(session): add session models, service, token utils, and cookie/request helpers
- Add UserSession and StatusPagePrivateUserSession database models
- Add UserSessionService to create/rotate/revoke sessions and issue access/refresh tokens
- Introduce SessionToken utilities (generate/hash tokens, TTLs, expiry computations)
- Update Cookie util to set/get access and refresh cookies for users and status pages and to remove auth cookies
- Add RequestMetadata helper to extract client IP, user-agent and device info
- Export new models in DatabaseModels Index and add RefreshToken name to CookieName
2025-11-10 11:12:33 +00:00
18 changed files with 1252 additions and 74 deletions

View File

@@ -118,6 +118,7 @@ import StatusPageHistoryChartBarColorRule from "./StatusPageHistoryChartBarColor
import StatusPageOwnerTeam from "./StatusPageOwnerTeam";
import StatusPageOwnerUser from "./StatusPageOwnerUser";
import StatusPagePrivateUser from "./StatusPagePrivateUser";
import StatusPagePrivateUserSession from "./StatusPagePrivateUserSession";
import StatusPageResource from "./StatusPageResource";
import StatusPageSCIM from "./StatusPageSCIM";
import StatusPageSSO from "./StatusPageSso";
@@ -152,6 +153,7 @@ import ServiceCatalogTelemetryService from "./ServiceCatalogTelemetryService";
import UserTotpAuth from "./UserTotpAuth";
import UserWebAuthn from "./UserWebAuthn";
import UserSession from "./UserSession";
import TelemetryIngestionKey from "./TelemetryIngestionKey";
@@ -266,6 +268,7 @@ const AllModelTypes: Array<{
StatusPageFooterLink,
StatusPageHeaderLink,
StatusPagePrivateUser,
StatusPagePrivateUserSession,
StatusPageHistoryChartBarColorRule,
ScheduledMaintenanceState,
@@ -318,6 +321,7 @@ const AllModelTypes: Array<{
UserOnCallLog,
UserOnCallLogTimeline,
UserNotificationSetting,
UserSession,
DataMigration,

View File

@@ -0,0 +1,278 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Project from "./Project";
import StatusPage from "./StatusPage";
import StatusPagePrivateUser from "./StatusPagePrivateUser";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnType from "../../Types/Database/ColumnType";
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import ColumnLength from "../../Types/Database/ColumnLength";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
@EnableDocumentation({
isMasterAdminApiDocs: true,
})
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [],
read: [],
delete: [],
update: [],
})
@Entity({
name: "StatusPagePrivateUserSession",
})
@TableMetadata({
tableName: "StatusPagePrivateUserSession",
singularName: "Status Page Session",
pluralName: "Status Page Sessions",
icon: IconProp.Clock,
tableDescription:
"Stores refresh tokens and metadata for authenticated status page sessions.",
})
@CurrentUserCanAccessRecordBy("statusPagePrivateUserId")
class StatusPagePrivateUserSession extends BaseModel {
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPagePrivateUserId",
type: TableColumnType.Entity,
title: "Private User",
description: "Status page private user associated with this session.",
modelType: StatusPagePrivateUser,
})
@ManyToOne(() => {
return StatusPagePrivateUser;
})
@JoinColumn({ name: "statusPagePrivateUserId" })
public statusPagePrivateUser?: StatusPagePrivateUser = undefined;
@Index()
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Private User ID",
description: "ID of the status page private user.",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPagePrivateUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "statusPageId",
type: TableColumnType.Entity,
title: "Status Page",
description: "Status page for which this session is valid.",
modelType: StatusPage,
})
@ManyToOne(() => {
return StatusPage;
})
@JoinColumn({ name: "statusPageId" })
public statusPage?: StatusPage = undefined;
@Index()
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Status Page ID",
description: "Identifier of the status page tied to this session.",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public statusPageId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
title: "Project",
description: "Project that owns the status page.",
modelType: Project,
})
@ManyToOne(() => {
return Project;
})
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Project ID",
description: "Project identifier for this session.",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@Index()
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.LongText,
title: "Refresh Token",
description: "Refresh token for this session.",
hashed: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
nullable: false,
unique: true,
})
public refreshToken?: string = undefined;
@Index()
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Expires At",
description: "When this refresh token expires.",
})
@Column({
type: ColumnType.Date,
nullable: false,
default: () => {
return "now()";
},
})
public refreshTokenExpiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Date,
title: "Last Used At",
description: "When this session was last used.",
})
@Column({
type: ColumnType.Date,
nullable: false,
default: () => {
return "now()";
},
})
public lastUsedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "IP Address",
description: "IP address associated with this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public ipAddress?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.LongText,
title: "User Agent",
description: "User agent information for this session.",
})
@Column({
type: ColumnType.LongText,
nullable: true,
})
public userAgent?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device",
description: "Device description captured for this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public device?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Is Revoked",
description: "Marks whether this session has been revoked.",
isDefaultValueColumn: true,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: false,
})
public isRevoked?: boolean = undefined;
}
export default StatusPagePrivateUserSession;

View File

@@ -0,0 +1,267 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Project from "./Project";
import User from "./User";
import Route from "../../Types/API/Route";
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import ColumnLength from "../../Types/Database/ColumnLength";
@EnableDocumentation({
isMasterAdminApiDocs: true,
})
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
delete: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@CrudApiEndpoint(new Route("/user-session"))
@Entity({
name: "UserSession",
})
@TableMetadata({
tableName: "UserSession",
singularName: "User Session",
pluralName: "User Sessions",
icon: IconProp.Clock,
tableDescription:
"Stores refresh tokens and metadata for authenticated dashboard sessions.",
})
@CurrentUserCanAccessRecordBy("userId")
class UserSession extends BaseModel {
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
title: "User",
description: "User associated with this session.",
modelType: User,
})
@ManyToOne(() => {
return User;
})
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@Index()
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "User ID",
description: "ID of the user associated with this session.",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public userId?: ObjectID = undefined;
@Index()
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.LongText,
title: "Refresh",
description: "Refresh token for this session.",
hashed: true,
required: true,
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
nullable: false,
unique: true,
})
public refreshToken?: string = undefined;
@Index()
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.Date,
title: "Expires At",
description: "When this refresh token expires.",
})
@Column({
type: ColumnType.Date,
nullable: false,
default: () => {
return "now()";
},
})
public refreshTokenExpiresAt?: Date = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.Date,
title: "Last Used At",
description: "When this session was last used.",
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.Date,
nullable: false,
default: () => {
return "now()";
},
})
public lastUsedAt?: Date = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "IP Address",
description: "IP address used when this session was created or refreshed.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public ipAddress?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.LongText,
title: "User Agent",
description: "User agent string associated with this session.",
})
@Column({
type: ColumnType.LongText,
nullable: true,
})
public userAgent?: string = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Device",
description: "Device description associated with this session.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public device?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Is Revoked",
description: "Marks whether this session has been revoked.",
isDefaultValueColumn: true,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: false,
})
public isRevoked?: boolean = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Global Login",
description:
"Indicates if this session was created via global login (non-SSO).",
isDefaultValueColumn: true,
defaultValue: true,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: true,
})
public isGlobalLogin?: boolean = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
title: "Project",
description: "Project tied to this session when authenticated via SSO.",
modelType: Project,
})
@ManyToOne(() => {
return Project;
})
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [Permission.CurrentUser],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Project ID",
description: "Project identifier for SSO sessions.",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
}
export default UserSession;

View File

@@ -1,14 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1762554602716 implements MigrationInterface {
public name = 'MigrationName1762554602716'
public name = "MigrationName1762554602716";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "jwtRefreshToken"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" ADD "jwtRefreshToken" character varying(100)`);
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "jwtRefreshToken"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "User" ADD "jwtRefreshToken" character varying(100)`,
);
}
}

View File

@@ -0,0 +1,103 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1762776676073 implements MigrationInterface {
public name = "MigrationName1762776676073";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "StatusPagePrivateUserSession" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "statusPagePrivateUserId" uuid NOT NULL, "statusPageId" uuid NOT NULL, "projectId" uuid NOT NULL, "refreshToken" character varying(500) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "ipAddress" character varying(100), "userAgent" character varying, "device" character varying(100), "isRevoked" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_12ce827a16d121bf6719260b8a9" UNIQUE ("refreshToken"), CONSTRAINT "PK_cbace84fe4c9712b94e571dc133" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_365d602943505272f8f651ff4e" ON "StatusPagePrivateUserSession" ("statusPagePrivateUserId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_7b8d9b6e068c045d56b47a484b" ON "StatusPagePrivateUserSession" ("statusPageId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_12ce827a16d121bf6719260b8a" ON "StatusPagePrivateUserSession" ("refreshToken") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_b317c0aa45b418b14515c31dff" ON "StatusPagePrivateUserSession" ("refreshTokenExpiresAt") `,
);
await queryRunner.query(
`CREATE TABLE "UserSession" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "userId" uuid NOT NULL, "refreshToken" character varying(500) NOT NULL, "refreshTokenExpiresAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "ipAddress" character varying(100), "userAgent" character varying, "device" character varying(100), "isRevoked" boolean NOT NULL DEFAULT false, "isGlobalLogin" boolean NOT NULL DEFAULT true, "projectId" uuid, CONSTRAINT "UQ_d66bd8342b0005c7192bdb17efc" UNIQUE ("refreshToken"), CONSTRAINT "PK_9dcd180f25755bab5fcebcbeb14" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_7353eaf92987aeaf38c2590e94" ON "UserSession" ("userId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_d66bd8342b0005c7192bdb17ef" ON "UserSession" ("refreshToken") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_f94389c48308f3d4f7258c24ed" ON "UserSession" ("refreshTokenExpiresAt") `,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_365d602943505272f8f651ff4e8" FOREIGN KEY ("statusPagePrivateUserId") REFERENCES "StatusPagePrivateUser"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" ADD CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "UserSession" ADD CONSTRAINT "FK_7353eaf92987aeaf38c2590e943" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "UserSession" ADD CONSTRAINT "FK_bb37d85f31bf216476fa5f93d9c" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "UserSession" DROP CONSTRAINT "FK_bb37d85f31bf216476fa5f93d9c"`,
);
await queryRunner.query(
`ALTER TABLE "UserSession" DROP CONSTRAINT "FK_7353eaf92987aeaf38c2590e943"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_ac5f4c13d6bc9696cbfb8e5a794"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_7b8d9b6e068c045d56b47a484be"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPagePrivateUserSession" DROP CONSTRAINT "FK_365d602943505272f8f651ff4e8"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_f94389c48308f3d4f7258c24ed"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_d66bd8342b0005c7192bdb17ef"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_7353eaf92987aeaf38c2590e94"`,
);
await queryRunner.query(`DROP TABLE "UserSession"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_b317c0aa45b418b14515c31dff"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_12ce827a16d121bf6719260b8a"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_7b8d9b6e068c045d56b47a484b"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_365d602943505272f8f651ff4e"`,
);
await queryRunner.query(`DROP TABLE "StatusPagePrivateUserSession"`);
}
}

View File

@@ -181,6 +181,7 @@ import { MigrationName1761232578396 } from "./1761232578396-MigrationName";
import { MigrationName1761834523183 } from "./1761834523183-MigrationName";
import { MigrationName1762181014879 } from "./1762181014879-MigrationName";
import { MigrationName1762554602716 } from "./1762554602716-MigrationName";
import { MigrationName1762776676073 } from "./1762776676073-MigrationName";
export default [
InitialMigration,
@@ -365,5 +366,6 @@ export default [
MigrationName1761232578396,
MigrationName1761834523183,
MigrationName1762181014879,
MigrationName1762554602716
MigrationName1762554602716,
MigrationName1762776676073,
];

View File

@@ -0,0 +1,292 @@
import DatabaseService from "./DatabaseService";
import UserSession from "../../Models/DatabaseModels/UserSession";
import User from "../../Models/DatabaseModels/User";
import ObjectID from "../../Types/ObjectID";
import HashedString from "../../Types/HashedString";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import JSONWebToken from "../Utils/JsonWebToken";
import OneUptimeDate from "../../Types/Date";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import {
ACCESS_TOKEN_TTL_SECONDS,
GeneratedAuthTokens,
SessionMetadata,
computeRefreshTokenExpiryDate,
generateRefreshToken,
getAccessTokenExpiresInSeconds,
getRefreshTokenExpiresInSeconds,
hashRefreshToken,
} from "../Utils/SessionToken";
import QueryDeepPartialEntity from "../../Types/Database/PartialEntity";
export type UserSessionWithTokens = GeneratedAuthTokens & {
session: UserSession;
};
export class Service extends DatabaseService<UserSession> {
public constructor() {
super(UserSession);
}
@CaptureSpan()
public async createSessionWithTokens(data: {
user: User;
isGlobalLogin: boolean;
projectId?: ObjectID;
metadata?: SessionMetadata;
props?: DatabaseCommonInteractionProps;
}): Promise<UserSessionWithTokens> {
const { user, isGlobalLogin, projectId, metadata, props } = data;
if (!user.id) {
throw new Error("User must have an id to create a session.");
}
const now: Date = OneUptimeDate.getCurrentDate();
const refreshToken: string = generateRefreshToken();
const refreshTokenHash: string = hashRefreshToken(refreshToken);
const refreshTokenExpiresAt: Date = computeRefreshTokenExpiryDate();
const session: UserSession = new UserSession();
session.userId = user.id;
session.isGlobalLogin = isGlobalLogin;
if (projectId) {
session.projectId = projectId;
}
session.refreshToken = new HashedString(refreshTokenHash, true);
session.refreshTokenExpiresAt = refreshTokenExpiresAt;
session.lastUsedAt = now;
if (metadata?.ipAddress !== undefined) {
session.ipAddress = metadata.ipAddress;
}
if (metadata?.userAgent !== undefined) {
session.userAgent = metadata.userAgent;
}
if (metadata?.device !== undefined) {
session.device = metadata.device;
}
const createdSession: UserSession = await this.create({
data: session,
props: props || {
isRoot: true,
},
});
const accessToken: string = this.createAccessToken({
user,
isGlobalLogin,
...(projectId ? { projectId } : {}),
});
return {
accessToken,
refreshToken,
accessTokenExpiresInSeconds: getAccessTokenExpiresInSeconds(),
refreshTokenExpiresInSeconds: getRefreshTokenExpiresInSeconds(),
refreshTokenExpiresAt,
session: createdSession,
};
}
@CaptureSpan()
public async rotateSessionTokens(data: {
session: UserSession;
user: User;
metadata?: SessionMetadata;
props?: DatabaseCommonInteractionProps;
}): Promise<UserSessionWithTokens> {
const { session, user, metadata, props } = data;
if (!session.id) {
throw new Error("Session id is required to rotate tokens.");
}
const refreshToken: string = generateRefreshToken();
const refreshTokenHash: string = hashRefreshToken(refreshToken);
const refreshTokenExpiresAt: Date = computeRefreshTokenExpiryDate();
const now: Date = OneUptimeDate.getCurrentDate();
session.refreshToken = new HashedString(refreshTokenHash, true);
session.refreshTokenExpiresAt = refreshTokenExpiresAt;
session.lastUsedAt = now;
if (metadata?.ipAddress !== undefined) {
session.ipAddress = metadata.ipAddress;
}
if (metadata?.userAgent !== undefined) {
session.userAgent = metadata.userAgent;
}
if (metadata?.device !== undefined) {
session.device = metadata.device;
}
session.isRevoked = false;
const updateData: QueryDeepPartialEntity<UserSession> = {
refreshToken: session.refreshToken,
refreshTokenExpiresAt,
lastUsedAt: now,
isRevoked: false,
};
if (metadata?.ipAddress !== undefined) {
updateData.ipAddress = metadata.ipAddress;
}
if (metadata?.userAgent !== undefined) {
updateData.userAgent = metadata.userAgent;
}
if (metadata?.device !== undefined) {
updateData.device = metadata.device;
}
await this.updateOneById({
id: session.id,
data: updateData,
props: props || {
isRoot: true,
},
});
const accessToken: string = this.createAccessToken({
user,
isGlobalLogin: Boolean(session.isGlobalLogin),
...(session.projectId ? { projectId: session.projectId } : {}),
});
return {
accessToken,
refreshToken,
accessTokenExpiresInSeconds: getAccessTokenExpiresInSeconds(),
refreshTokenExpiresInSeconds: getRefreshTokenExpiresInSeconds(),
refreshTokenExpiresAt,
session,
};
}
@CaptureSpan()
public async getActiveSessionByRefreshToken(data: {
refreshToken: string;
props?: DatabaseCommonInteractionProps;
}): Promise<UserSession | null> {
const { refreshToken, props } = data;
if (!refreshToken) {
return null;
}
const refreshTokenHash: string = hashRefreshToken(refreshToken);
const session: UserSession | null = await this.findOneBy({
query: {
refreshToken: new HashedString(refreshTokenHash, true),
isRevoked: false,
},
select: {
_id: true,
userId: true,
isGlobalLogin: true,
projectId: true,
refreshTokenExpiresAt: true,
},
props: props || {
isRoot: true,
},
});
if (!session) {
return null;
}
if (session.refreshTokenExpiresAt) {
const refreshExpired: boolean = OneUptimeDate.hasExpired(
session.refreshTokenExpiresAt,
);
if (refreshExpired) {
await this.revokeSessionById({
sessionId: session.id!,
...(props ? { props } : {}),
});
return null;
}
}
return session;
}
@CaptureSpan()
public async revokeSessionByRefreshToken(data: {
refreshToken: string;
props?: DatabaseCommonInteractionProps;
}): Promise<void> {
const { refreshToken, props } = data;
if (!refreshToken) {
return;
}
const refreshTokenHash: string = hashRefreshToken(refreshToken);
await this.updateOneBy({
query: {
refreshToken: new HashedString(refreshTokenHash, true),
},
data: {
isRevoked: true,
refreshTokenExpiresAt: OneUptimeDate.getCurrentDate(),
},
props: props || {
isRoot: true,
},
});
}
@CaptureSpan()
public async revokeSessionById(data: {
sessionId: ObjectID;
props?: DatabaseCommonInteractionProps;
}): Promise<void> {
const { sessionId, props } = data;
await this.updateOneById({
id: sessionId,
data: {
isRevoked: true,
refreshTokenExpiresAt: OneUptimeDate.getCurrentDate(),
},
props: props || {
isRoot: true,
},
});
}
private createAccessToken(data: {
user: User;
isGlobalLogin: boolean;
projectId?: ObjectID;
}): string {
const { user, isGlobalLogin, projectId } = data;
return JSONWebToken.sign({
data: {
userId: user.id!,
email: user.email!,
name: user.name!,
timezone: user.timezone || null,
isMasterAdmin: Boolean(user.isMasterAdmin),
isGlobalLogin,
projectId: projectId || undefined,
},
expiresInSeconds: ACCESS_TOKEN_TTL_SECONDS,
});
}
}
export default new Service();

View File

@@ -3,6 +3,7 @@ import Dictionary from "../../Types/Dictionary";
import ObjectID from "../../Types/ObjectID";
import { CookieOptions } from "express";
import JSONWebToken from "./JsonWebToken";
import StatusPagePrivateUser from "../../Models/DatabaseModels/StatusPagePrivateUser";
import User from "../../Models/DatabaseModels/User";
import OneUptimeDate from "../../Types/Date";
import PositiveNumber from "../../Types/PositiveNumber";
@@ -57,92 +58,186 @@ export default class CookieUtil {
public static setUserCookie(data: {
expressResponse: ExpressResponse;
user: User;
isGlobalLogin: boolean;
accessToken: string;
refreshToken: string;
accessTokenExpiresInSeconds: number;
refreshTokenExpiresInSeconds: number;
}): void {
const { expressResponse: res, user, isGlobalLogin } = data;
const {
expressResponse: res,
user,
accessToken,
refreshToken,
accessTokenExpiresInSeconds,
refreshTokenExpiresInSeconds,
} = data;
const token: string = JSONWebToken.signUserLoginToken({
tokenData: {
userId: user.id!,
email: user.email!,
name: user.name!,
timezone: user.timezone || null,
isMasterAdmin: user.isMasterAdmin!,
isGlobalLogin: isGlobalLogin, // This is a general login without SSO. So, we will set this to true. This will give access to all the projects that dont require SSO.
},
expiresInSeconds: OneUptimeDate.getSecondsInDays(new PositiveNumber(30)),
});
const accessTokenMaxAge: number = Math.max(
0,
Math.round(accessTokenExpiresInSeconds * 1000),
);
const refreshTokenMaxAge: number = Math.max(
0,
Math.round(refreshTokenExpiresInSeconds * 1000),
);
// Set a cookie with token.
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), token, {
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), accessToken, {
maxAge: accessTokenMaxAge,
httpOnly: true,
sameSite: "lax",
path: "/",
});
CookieUtil.setCookie(
res,
CookieUtil.getUserRefreshTokenKey(),
refreshToken,
{
maxAge: refreshTokenMaxAge,
httpOnly: true,
sameSite: "lax",
path: "/",
},
);
if (user.id) {
// set user id cookie
CookieUtil.setCookie(res, CookieName.UserID, user.id!.toString(), {
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
maxAge: refreshTokenMaxAge,
httpOnly: false,
sameSite: "lax",
path: "/",
});
}
if (user.email) {
// set user email cookie
CookieUtil.setCookie(
res,
CookieName.Email,
user.email?.toString() || "",
{
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
maxAge: refreshTokenMaxAge,
httpOnly: false,
sameSite: "lax",
path: "/",
},
);
}
if (user.name) {
// set user name cookie
CookieUtil.setCookie(res, CookieName.Name, user.name?.toString() || "", {
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
maxAge: refreshTokenMaxAge,
httpOnly: false,
sameSite: "lax",
path: "/",
});
}
if (user.timezone) {
// set user timezone cookie
CookieUtil.setCookie(
res,
CookieName.Timezone,
user.timezone?.toString() || "",
{
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
maxAge: refreshTokenMaxAge,
httpOnly: false,
sameSite: "lax",
path: "/",
},
);
}
if (user.isMasterAdmin) {
// set user isMasterAdmin cookie
CookieUtil.setCookie(
res,
CookieName.IsMasterAdmin,
user.isMasterAdmin?.toString() || "",
{
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
maxAge: refreshTokenMaxAge,
httpOnly: false,
sameSite: "lax",
path: "/",
},
);
}
if (user.profilePictureId) {
// set user profile picture id cookie
CookieUtil.setCookie(
res,
CookieName.ProfilePicID,
user.profilePictureId?.toString() || "",
{
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
maxAge: refreshTokenMaxAge,
httpOnly: false,
sameSite: "lax",
path: "/",
},
);
}
}
@CaptureSpan()
public static setStatusPagePrivateUserCookie(data: {
expressResponse: ExpressResponse;
user: StatusPagePrivateUser;
statusPageId: ObjectID;
accessToken: string;
refreshToken: string;
accessTokenExpiresInSeconds: number;
refreshTokenExpiresInSeconds: number;
}): void {
const {
expressResponse: res,
user,
statusPageId,
accessToken,
refreshToken,
accessTokenExpiresInSeconds,
refreshTokenExpiresInSeconds,
} = data;
const accessTokenMaxAge: number = Math.max(
0,
Math.round(accessTokenExpiresInSeconds * 1000),
);
const refreshTokenMaxAge: number = Math.max(
0,
Math.round(refreshTokenExpiresInSeconds * 1000),
);
CookieUtil.setCookie(
res,
CookieUtil.getUserTokenKey(statusPageId),
accessToken,
{
maxAge: accessTokenMaxAge,
httpOnly: true,
sameSite: "lax",
path: "/",
},
);
CookieUtil.setCookie(
res,
CookieUtil.getUserRefreshTokenKey(statusPageId),
refreshToken,
{
maxAge: refreshTokenMaxAge,
httpOnly: true,
sameSite: "lax",
path: "/",
},
);
if (user.email) {
CookieUtil.setCookie(
res,
`${CookieName.Email}-${statusPageId.toString()}`,
user.email?.toString() || "",
{
maxAge: refreshTokenMaxAge,
httpOnly: false,
sameSite: "lax",
path: "/",
},
);
}
@@ -190,6 +285,15 @@ export default class CookieUtil {
return `${CookieName.Token}-${id.toString()}`;
}
@CaptureSpan()
public static getUserRefreshTokenKey(id?: ObjectID): string {
if (!id) {
return CookieName.RefreshToken;
}
return `${CookieName.RefreshToken}-${id.toString()}`;
}
@CaptureSpan()
public static getUserSSOKey(id: ObjectID): string {
return `${this.getSSOKey()}${id.toString()}`;
@@ -211,4 +315,44 @@ export default class CookieUtil {
this.removeCookie(res, key);
}
}
@CaptureSpan()
public static getUserRefreshTokenFromExpressRequest(
req: ExpressRequest,
): string | undefined {
return this.getCookieFromExpressRequest(req, this.getUserRefreshTokenKey());
}
@CaptureSpan()
public static getStatusPageRefreshTokenFromExpressRequest(
req: ExpressRequest,
statusPageId: ObjectID,
): string | undefined {
return this.getCookieFromExpressRequest(
req,
this.getUserRefreshTokenKey(statusPageId),
);
}
@CaptureSpan()
public static removeUserAuthCookies(res: ExpressResponse): void {
this.removeCookie(res, this.getUserTokenKey());
this.removeCookie(res, this.getUserRefreshTokenKey());
this.removeCookie(res, CookieName.UserID);
this.removeCookie(res, CookieName.Email);
this.removeCookie(res, CookieName.Name);
this.removeCookie(res, CookieName.Timezone);
this.removeCookie(res, CookieName.IsMasterAdmin);
this.removeCookie(res, CookieName.ProfilePicID);
}
@CaptureSpan()
public static removeStatusPageAuthCookies(
res: ExpressResponse,
statusPageId: ObjectID,
): void {
this.removeCookie(res, this.getUserTokenKey(statusPageId));
this.removeCookie(res, this.getUserRefreshTokenKey(statusPageId));
this.removeCookie(res, `${CookieName.Email}-${statusPageId.toString()}`);
}
}

View File

@@ -0,0 +1,56 @@
import { ExpressRequest } from "./Express";
export default class RequestMetadata {
public static getClientIp(req: ExpressRequest): string | undefined {
const forwardedFor: string | undefined =
(req.headers["x-forwarded-for"] as string | undefined) || undefined;
const realIp: string | undefined =
(req.headers["x-real-ip"] as string | undefined) || undefined;
const socketAddress: string | undefined =
req.socket?.remoteAddress || undefined;
const reqIp: string | undefined = req.ip;
const reqIpsFirst: string | undefined = Array.isArray(req.ips)
? req.ips[0]
: undefined;
const candidate: Array<string | undefined> = [
forwardedFor,
realIp,
socketAddress,
reqIp,
reqIpsFirst,
];
for (const value of candidate) {
if (value && value.trim()) {
return value.split(",")[0]?.trim();
}
}
return undefined;
}
public static getUserAgent(req: ExpressRequest): string | undefined {
const userAgent: string | undefined = req.headers["user-agent"] as
| string
| undefined;
return userAgent?.trim() || undefined;
}
public static getDevice(req: ExpressRequest): string | undefined {
const deviceHeader: string | undefined =
(req.headers["x-device"] as string | undefined) || undefined;
if (deviceHeader && deviceHeader.trim()) {
return deviceHeader.trim();
}
const userAgent: string | undefined = this.getUserAgent(req);
if (userAgent) {
return userAgent;
}
return undefined;
}
}

View File

@@ -0,0 +1,42 @@
import OneUptimeDate from "../../Types/Date";
import { createHash, randomBytes } from "crypto";
export const ACCESS_TOKEN_TTL_SECONDS: number = 15 * 60; // 15 minutes
export const REFRESH_TOKEN_TTL_SECONDS: number = 30 * 24 * 60 * 60; // 30 days
export type SessionMetadata = {
ipAddress?: string;
userAgent?: string;
device?: string;
};
export type GeneratedAuthTokens = {
accessToken: string;
refreshToken: string;
accessTokenExpiresInSeconds: number;
refreshTokenExpiresInSeconds: number;
refreshTokenExpiresAt: Date;
};
export const generateRefreshToken = (): string => {
return randomBytes(48).toString("hex");
};
export const hashRefreshToken = (token: string): string => {
return createHash("sha256").update(token).digest("hex");
};
export const computeRefreshTokenExpiryDate = (): Date => {
return OneUptimeDate.addRemoveSeconds(
OneUptimeDate.getCurrentDate(),
REFRESH_TOKEN_TTL_SECONDS,
);
};
export const getRefreshTokenExpiresInSeconds = (): number => {
return REFRESH_TOKEN_TTL_SECONDS;
};
export const getAccessTokenExpiresInSeconds = (): number => {
return ACCESS_TOKEN_TTL_SECONDS;
};

View File

@@ -2,6 +2,7 @@ enum CookieName {
UserID = "user-id",
Email = "user-email",
Token = "user-token",
RefreshToken = "user-refresh-token",
Name = "user-name",
Timezone = "user-timezone",
IsMasterAdmin = "user-is-master-admin",

View File

@@ -75,8 +75,7 @@ export const WORKFLOW_HOSTNAME: Hostname = Hostname.fromString(HOST);
export const PROBE_INGEST_HOSTNAME: Hostname = Hostname.fromString(HOST);
export const TELEMETRY_HOSTNAME: Hostname =
Hostname.fromString(HOST);
export const TELEMETRY_HOSTNAME: Hostname = Hostname.fromString(HOST);
export const INCOMING_REQUEST_INGEST_HOSTNAME: Hostname =
Hostname.fromString(HOST);

View File

@@ -60,7 +60,7 @@ class BaseAPI extends API {
);
}
return Promise.resolve(headers);
return headers;
}
protected static override getHeaders(): Headers {

View File

@@ -3,11 +3,7 @@ import { Page, expect, test } from "@playwright/test";
import URL from "Common/Types/API/URL";
test.describe("check live and health check of telemetry", () => {
test("check if telemetry status is ok", async ({
page,
}: {
page: Page;
}) => {
test("check if telemetry status is ok", async ({ page }: { page: Page }) => {
page.setDefaultNavigationTimeout(120000); // 2 minutes
await page.goto(
`${URL.fromString(BASE_URL.toString())
@@ -18,11 +14,7 @@ test.describe("check live and health check of telemetry", () => {
expect(content).toContain('{"status":"ok"}');
});
test("check if telemetry is ready", async ({
page,
}: {
page: Page;
}) => {
test("check if telemetry is ready", async ({ page }: { page: Page }) => {
page.setDefaultNavigationTimeout(120000); // 2 minutes
await page.goto(
`${URL.fromString(BASE_URL.toString())
@@ -33,11 +25,7 @@ test.describe("check live and health check of telemetry", () => {
expect(content).toContain('{"status":"ok"}');
});
test("check if telemetry is live", async ({
page,
}: {
page: Page;
}) => {
test("check if telemetry is live", async ({ page }: { page: Page }) => {
page.setDefaultNavigationTimeout(120000); // 2 minutes
await page.goto(
`${URL.fromString(BASE_URL.toString())

View File

@@ -28,17 +28,25 @@ const parseBatchSize: ParseBatchSizeFunction = (
return parsed;
};
export const TELEMETRY_LOG_FLUSH_BATCH_SIZE: number =
parseBatchSize("TELEMETRY_LOG_FLUSH_BATCH_SIZE", 1000);
export const TELEMETRY_LOG_FLUSH_BATCH_SIZE: number = parseBatchSize(
"TELEMETRY_LOG_FLUSH_BATCH_SIZE",
1000,
);
export const TELEMETRY_METRIC_FLUSH_BATCH_SIZE: number =
parseBatchSize("TELEMETRY_METRIC_FLUSH_BATCH_SIZE", 750);
export const TELEMETRY_METRIC_FLUSH_BATCH_SIZE: number = parseBatchSize(
"TELEMETRY_METRIC_FLUSH_BATCH_SIZE",
750,
);
export const TELEMETRY_TRACE_FLUSH_BATCH_SIZE: number =
parseBatchSize("TELEMETRY_TRACE_FLUSH_BATCH_SIZE", 750);
export const TELEMETRY_TRACE_FLUSH_BATCH_SIZE: number = parseBatchSize(
"TELEMETRY_TRACE_FLUSH_BATCH_SIZE",
750,
);
export const TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE: number =
parseBatchSize("TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE", 500);
export const TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE: number = parseBatchSize(
"TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE",
500,
);
/*
* Some telemetry batches can be large and take >30s (BullMQ default lock) to process.

View File

@@ -306,9 +306,7 @@ export default class OtelLogsIngestService extends OtelIngestBaseService {
dbLogs.push(logRow);
totalLogsProcessed++;
if (
dbLogs.length >= TELEMETRY_LOG_FLUSH_BATCH_SIZE
) {
if (dbLogs.length >= TELEMETRY_LOG_FLUSH_BATCH_SIZE) {
await this.flushLogsBuffer(dbLogs);
}
} catch (logError) {

View File

@@ -312,8 +312,7 @@ export default class OtelMetricsIngestService extends OtelIngestBaseService {
totalMetricsProcessed++;
if (
dbMetrics.length >=
TELEMETRY_METRIC_FLUSH_BATCH_SIZE
dbMetrics.length >= TELEMETRY_METRIC_FLUSH_BATCH_SIZE
) {
await this.flushMetricsBuffer(dbMetrics);
}

View File

@@ -361,16 +361,12 @@ export default class OtelTracesIngestService extends OtelIngestBaseService {
dbSpans.push(spanRow);
totalSpansProcessed++;
if (
dbSpans.length >=
TELEMETRY_TRACE_FLUSH_BATCH_SIZE
) {
if (dbSpans.length >= TELEMETRY_TRACE_FLUSH_BATCH_SIZE) {
await this.flushSpansBuffer(dbSpans);
}
if (
dbExceptions.length >=
TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE
dbExceptions.length >= TELEMETRY_EXCEPTION_FLUSH_BATCH_SIZE
) {
await this.flushExceptionsBuffer(dbExceptions);
}