1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

Merge branch 'main' into quickstart-update

This commit is contained in:
melindafekete 2025-07-04 14:44:48 +02:00
commit 8328e83d11
No known key found for this signature in database
15 changed files with 119 additions and 109 deletions

View File

@ -1,18 +0,0 @@
import type { FC } from 'react';
import { NameWithChangeInfo } from './NameWithChangeInfo/NameWithChangeInfo.tsx';
type ChangeSegmentNameProps = {
name: string;
previousName?: string;
};
export const ChangeSegmentName: FC<ChangeSegmentNameProps> = ({
name,
previousName,
}) => {
return (
<span>
<NameWithChangeInfo newName={name} previousName={previousName} />
</span>
);
};

View File

@ -1,30 +0,0 @@
import type { FC } from 'react';
import { formatStrategyName } from 'utils/strategyNames';
import { styled } from '@mui/material';
import { textTruncated } from 'themes/themeStyles';
import { NameWithChangeInfo } from './NameWithChangeInfo/NameWithChangeInfo.tsx';
import { Truncator } from 'component/common/Truncator/Truncator.tsx';
type ChangeStrategyNameProps = {
name: string;
title?: string;
previousTitle?: string;
};
const Truncated = styled('span')(() => ({
...textTruncated,
maxWidth: 500,
}));
export const ChangeStrategyName: FC<ChangeStrategyNameProps> = ({
name,
title,
previousTitle,
}) => {
return (
<Truncated>
<Truncator component='span'>{formatStrategyName(name)}</Truncator>
<NameWithChangeInfo newName={title} previousName={previousTitle} />
</Truncated>
);
};

View File

@ -109,7 +109,7 @@ export const EnvironmentStrategyExecutionOrder = ({
</NewChangeItemInfo> </NewChangeItemInfo>
<div> <div>
<TabList> <TabList>
<Tab>Change</Tab> <Tab>View change</Tab>
<Tab>View diff</Tab> <Tab>View diff</Tab>
</TabList> </TabList>
{actions} {actions}

View File

@ -6,7 +6,6 @@ import { textTruncated } from 'themes/themeStyles';
const Truncated = styled('span')(() => ({ const Truncated = styled('span')(() => ({
...textTruncated, ...textTruncated,
maxWidth: 500, maxWidth: 500,
display: 'block',
})); }));
const NewName = styled(Typography)<TypographyProps>({ const NewName = styled(Typography)<TypographyProps>({

View File

@ -89,7 +89,7 @@ const StartMilestone: FC<{
</ChangeItemInfo> </ChangeItemInfo>
<div> <div>
<TabList> <TabList>
<Tab>Change</Tab> <Tab>View change</Tab>
<Tab>View diff</Tab> <Tab>View diff</Tab>
</TabList> </TabList>
{actions} {actions}
@ -202,7 +202,7 @@ const AddReleasePlan: FC<{
</ChangeItemInfo> </ChangeItemInfo>
<div> <div>
<TabList> <TabList>
<Tab>Changes</Tab> <Tab>View change</Tab>
<Tab>View diff</Tab> <Tab>View diff</Tab>
</TabList> </TabList>
{actions} {actions}

View File

@ -17,7 +17,7 @@ import {
Deleted, Deleted,
} from './Change.styles.tsx'; } from './Change.styles.tsx';
import { SegmentDiff } from './SegmentDiff.tsx'; import { SegmentDiff } from './SegmentDiff.tsx';
import { ChangeSegmentName } from './ChangeSegmentName.tsx'; import { NameWithChangeInfo } from './NameWithChangeInfo/NameWithChangeInfo.tsx';
const ActionsContainer = styled('div')(({ theme }) => ({ const ActionsContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -57,7 +57,7 @@ export const SegmentChangeDetails: FC<{
const actionsWithTabs = ( const actionsWithTabs = (
<ActionsContainer> <ActionsContainer>
<TabList> <TabList>
<Tab>Change</Tab> <Tab>View change</Tab>
<Tab>View diff</Tab> <Tab>View diff</Tab>
</TabList> </TabList>
{actions} {actions}
@ -72,8 +72,8 @@ export const SegmentChangeDetails: FC<{
<ChangeItemWrapper> <ChangeItemWrapper>
<ChangeItemInfo> <ChangeItemInfo>
<Deleted>Deleting segment</Deleted> <Deleted>Deleting segment</Deleted>
<ChangeSegmentName <NameWithChangeInfo
name={change.payload.name} newName={change.payload.name}
previousName={previousName} previousName={previousName}
/> />
</ChangeItemInfo> </ChangeItemInfo>
@ -102,8 +102,8 @@ export const SegmentChangeDetails: FC<{
<ChangeItemWrapper> <ChangeItemWrapper>
<ChangeItemInfo> <ChangeItemInfo>
<Action>Editing segment</Action> <Action>Editing segment</Action>
<ChangeSegmentName <NameWithChangeInfo
name={change.payload.name} newName={change.payload.name}
previousName={previousName} previousName={previousName}
/> />
</ChangeItemInfo> </ChangeItemInfo>

View File

@ -243,7 +243,6 @@ test('Deleting strategy before change request is applied diffs against current s
); );
await screen.findByText('Deleting strategy'); await screen.findByText('Deleting strategy');
await screen.findByText('Gradual rollout');
await screen.findByText('current_title'); await screen.findByText('current_title');
await screen.findByText('current_variant'); await screen.findByText('current_variant');
@ -299,7 +298,6 @@ test('Deleting strategy after change request is applied diffs against the snapsh
); );
await screen.findByText('Deleting strategy'); await screen.findByText('Deleting strategy');
await screen.findByText('Gradual rollout');
await screen.findByText('snapshot_title'); await screen.findByText('snapshot_title');
expect(screen.queryByText('current_title')).not.toBeInTheDocument(); expect(screen.queryByText('current_title')).not.toBeInTheDocument();

View File

@ -18,7 +18,6 @@ import { EnvironmentVariantsTable } from 'component/feature/FeatureView/FeatureV
import { ChangeOverwriteWarning } from './ChangeOverwriteWarning/ChangeOverwriteWarning.tsx'; import { ChangeOverwriteWarning } from './ChangeOverwriteWarning/ChangeOverwriteWarning.tsx';
import type { IFeatureStrategy } from 'interfaces/strategy'; import type { IFeatureStrategy } from 'interfaces/strategy';
import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx'; import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
import { ChangeStrategyName } from './ChangeStrategyName.tsx';
import { StrategyDiff } from './StrategyDiff.tsx'; import { StrategyDiff } from './StrategyDiff.tsx';
import { import {
Action, Action,
@ -27,6 +26,7 @@ import {
ChangeItemWrapper, ChangeItemWrapper,
Deleted, Deleted,
} from './Change.styles.tsx'; } from './Change.styles.tsx';
import { NameWithChangeInfo } from './NameWithChangeInfo/NameWithChangeInfo.tsx';
const StyledBox: FC<{ children?: React.ReactNode }> = styled(Box)( const StyledBox: FC<{ children?: React.ReactNode }> = styled(Box)(
({ theme }) => ({ ({ theme }) => ({
@ -103,10 +103,6 @@ const DeleteStrategy: FC<{
currentStrategy: IFeatureStrategy | undefined; currentStrategy: IFeatureStrategy | undefined;
actions?: ReactNode; actions?: ReactNode;
}> = ({ change, changeRequestState, currentStrategy, actions }) => { }> = ({ change, changeRequestState, currentStrategy, actions }) => {
const name =
changeRequestState === 'Applied'
? change.payload?.snapshot?.name
: currentStrategy?.name;
const title = const title =
changeRequestState === 'Applied' changeRequestState === 'Applied'
? change.payload?.snapshot?.title ? change.payload?.snapshot?.title
@ -121,7 +117,10 @@ const DeleteStrategy: FC<{
<ChangeItemWrapper> <ChangeItemWrapper>
<ChangeItemInfo> <ChangeItemInfo>
<Deleted>Deleting strategy</Deleted> <Deleted>Deleting strategy</Deleted>
<ChangeStrategyName name={name || ''} title={title} /> <NameWithChangeInfo
newName={title}
previousName={referenceStrategy?.title}
/>
</ChangeItemInfo> </ChangeItemInfo>
{actions} {actions}
</ChangeItemWrapper> </ChangeItemWrapper>
@ -175,10 +174,9 @@ const UpdateStrategy: FC<{
wasDisabled={currentStrategy?.disabled} wasDisabled={currentStrategy?.disabled}
willBeDisabled={change.payload?.disabled} willBeDisabled={change.payload?.disabled}
/> />
<ChangeStrategyName <NameWithChangeInfo
name={change.payload.name} newName={change.payload.title}
title={change.payload.title} previousName={previousTitle}
previousTitle={previousTitle}
/> />
</ChangeItemInfo> </ChangeItemInfo>
{actions} {actions}
@ -248,10 +246,7 @@ const AddStrategy: FC<{
<AddedStrategy disabled={change.payload?.disabled}> <AddedStrategy disabled={change.payload?.disabled}>
Adding {isDefaultChange && 'default'} strategy Adding {isDefaultChange && 'default'} strategy
</AddedStrategy> </AddedStrategy>
<ChangeStrategyName <NameWithChangeInfo newName={change.payload.title} />
name={change.payload.name}
title={change.payload.title}
/>
<DisabledEnabledState <DisabledEnabledState
disabled disabled
show={change.payload?.disabled === true} show={change.payload?.disabled === true}
@ -321,7 +316,7 @@ export const StrategyChange: FC<{
const actionsWithTabs = ( const actionsWithTabs = (
<ActionsContainer> <ActionsContainer>
<TabList> <TabList>
<Tab>Change</Tab> <Tab>View change</Tab>
<Tab>View diff</Tab> <Tab>View diff</Tab>
</TabList> </TabList>
{actions} {actions}

View File

@ -1,6 +1,6 @@
import { useState, type VFC } from 'react'; import { useState, type VFC } from 'react';
import { Box, Button, styled, Typography } from '@mui/material'; import { Box, Button, styled, Typography } from '@mui/material';
import { DynamicSidebarModal } from 'component/common/SidebarModal/SidebarModal'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import CheckCircle from '@mui/icons-material/CheckCircle'; import CheckCircle from '@mui/icons-material/CheckCircle';
@ -22,7 +22,6 @@ interface IChangeRequestSidebarProps {
const StyledPageContent = styled(PageContent)(({ theme }) => ({ const StyledPageContent = styled(PageContent)(({ theme }) => ({
height: '100vh', height: '100vh',
overflow: 'auto', overflow: 'auto',
minWidth: '50vw',
padding: theme.spacing(6), padding: theme.spacing(6),
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
padding: theme.spacing(4, 2), padding: theme.spacing(4, 2),
@ -106,11 +105,7 @@ export const ChangeRequestSidebar: VFC<IChangeRequestSidebarProps> = ({
if (!loading && !data) { if (!loading && !data) {
return ( return (
<DynamicSidebarModal <SidebarModal open={open} onClose={onClose} label='Review changes'>
open={open}
onClose={onClose}
label='Review changes'
>
<StyledPageContent <StyledPageContent
disableBorder={true} disableBorder={true}
header={<PageHeader titleElement='Review your changes' />} header={<PageHeader titleElement='Review your changes' />}
@ -119,16 +114,12 @@ export const ChangeRequestSidebar: VFC<IChangeRequestSidebarProps> = ({
{/* FIXME: empty state */} {/* FIXME: empty state */}
<BackButton onClick={onClose}>Close</BackButton> <BackButton onClick={onClose}>Close</BackButton>
</StyledPageContent> </StyledPageContent>
</DynamicSidebarModal> </SidebarModal>
); );
} }
return ( return (
<DynamicSidebarModal <SidebarModal open={open} onClose={onClose} label='Review changes'>
open={open}
onClose={onClose}
label='Review changes'
>
<StyledPageContent <StyledPageContent
disableBorder={true} disableBorder={true}
header={<ReviewChangesHeader />} header={<ReviewChangesHeader />}
@ -159,6 +150,6 @@ export const ChangeRequestSidebar: VFC<IChangeRequestSidebarProps> = ({
</ChangeRequestPlausibleProvider> </ChangeRequestPlausibleProvider>
))} ))}
</StyledPageContent> </StyledPageContent>
</DynamicSidebarModal> </SidebarModal>
); );
}; };

View File

@ -154,6 +154,27 @@ describe('validators', () => {
]); ]);
}, },
); );
test.each(multipleValueOperators)(
'multi-value operator %s should reject fully duplicate inputs and accept new values',
(operator) => {
const initial: IConstraint = {
contextName: 'context-field',
operator: operator,
values: ['a', 'b'],
};
const { result } = renderHook(() =>
useEditableConstraint(initial, () => {}),
);
checkValidator(result.current.validator, [
['a', false],
[['a', 'c'], true],
[['a', 'b'], false],
]);
},
);
}); });
describe('legal values', () => { describe('legal values', () => {

View File

@ -4,6 +4,7 @@ import type { IConstraint } from 'interfaces/strategy';
import { import {
type EditableConstraint, type EditableConstraint,
fromIConstraint, fromIConstraint,
isMultiValueConstraint,
isSingleValueConstraint, isSingleValueConstraint,
toIConstraint, toIConstraint,
} from './editable-constraint-type.ts'; } from './editable-constraint-type.ts';
@ -16,8 +17,8 @@ import {
type ConstraintUpdateAction, type ConstraintUpdateAction,
} from './constraint-reducer.ts'; } from './constraint-reducer.ts';
import { import {
type ConstraintValidationResult,
constraintValidator, constraintValidator,
type ConstraintValidationResult,
} from './constraint-validator.ts'; } from './constraint-validator.ts';
import { import {
getDeletedLegalValues, getDeletedLegalValues,
@ -76,7 +77,20 @@ export const useEditableConstraint = (
[JSON.stringify(context), localConstraint.contextName], [JSON.stringify(context), localConstraint.contextName],
); );
const validator = constraintValidator(localConstraint.operator); const baseValidator = constraintValidator(localConstraint.operator);
const validator = (...values: string[]) => {
if (
isMultiValueConstraint(localConstraint) &&
values.every((value) => localConstraint.values.has(value))
) {
if (values.length === 1) {
return [false, `${values[0]} is already added.`];
}
return [false, `All the values are already added`];
}
return baseValidator(...values);
};
useEffect(() => { useEffect(() => {
if ( if (
@ -104,7 +118,7 @@ export const useEditableConstraint = (
isSingleValueConstraint(localConstraint) isSingleValueConstraint(localConstraint)
) { ) {
return getInvalidLegalValues( return getInvalidLegalValues(
(value) => validator(value)[0], (value) => baseValidator(value)[0],
contextDefinition.legalValues, contextDefinition.legalValues,
); );
} }

View File

@ -103,10 +103,7 @@ export default async function getApp(
if (config.enableOAS && services.openApiService) { if (config.enableOAS && services.openApiService) {
services.openApiService.useDocs(app); services.openApiService.useDocs(app);
} }
// Support CORS preflight requests for the frontend endpoints. app.use(
// Preflight requests should not have Authorization headers,
// so this must be handled before the API token middleware.
app.options(
`${baseUriPath}/api/frontend*`, `${baseUriPath}/api/frontend*`,
corsOriginMiddleware(services, config), corsOriginMiddleware(services, config),
); );

View File

@ -28,6 +28,8 @@ import type { ProjectActivitySchema } from '../../openapi/index.js';
import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type.js'; import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type.js';
import { applyGenericQueryParams } from '../feature-search/search-utils.js'; import { applyGenericQueryParams } from '../feature-search/search-utils.js';
import type { ITag } from '../../tags/index.js'; import type { ITag } from '../../tags/index.js';
import metricsHelper from '../../util/metrics-helper.js';
import { DB_TIME } from '../../metric-events.js';
const EVENT_COLUMNS = [ const EVENT_COLUMNS = [
'id', 'id',
@ -113,26 +115,38 @@ export class EventStore implements IEventStore {
private logger: Logger; private logger: Logger;
private metricTimer: Function;
// a new DB has to be injected per transaction // a new DB has to be injected per transaction
constructor(db: Db, getLogger: LogProvider) { constructor(db: Db, getLogger: LogProvider) {
this.db = db; this.db = db;
this.logger = getLogger('event-store'); this.logger = getLogger('event-store');
this.metricTimer = (action) =>
metricsHelper.wrapTimer(this.eventEmitter, DB_TIME, {
store: 'event',
action,
});
} }
async store(event: IBaseEvent): Promise<void> { async store(event: IBaseEvent): Promise<void> {
const stopTimer = this.metricTimer('store');
try { try {
await this.db(TABLE) await this.db(TABLE)
.insert(this.eventToDbRow(event)) .insert(this.eventToDbRow(event))
.returning(EVENT_COLUMNS); .returning(EVENT_COLUMNS);
} catch (error: unknown) { } catch (error: unknown) {
this.logger.warn(`Failed to store "${event.type}" event: ${error}`); this.logger.warn(`Failed to store "${event.type}" event: ${error}`);
} finally {
stopTimer();
} }
} }
async count(): Promise<number> { async count(): Promise<number> {
const stopTimer = this.metricTimer('count');
const count = await this.db(TABLE) const count = await this.db(TABLE)
.count<Record<string, number>>() .count<Record<string, number>>()
.first(); .first();
stopTimer();
if (!count) { if (!count) {
return 0; return 0;
} }
@ -147,8 +161,10 @@ export class EventStore implements IEventStore {
queryParams: IQueryParam[], queryParams: IQueryParam[],
query?: IEventSearchParams['query'], query?: IEventSearchParams['query'],
): Promise<number> { ): Promise<number> {
const stopTimer = this.metricTimer('searchEventsCount');
const searchQuery = this.buildSearchQuery(queryParams, query); const searchQuery = this.buildSearchQuery(queryParams, query);
const count = await searchQuery.count().first(); const count = await searchQuery.count().first();
stopTimer();
if (!count) { if (!count) {
return 0; return 0;
} }
@ -160,6 +176,7 @@ export class EventStore implements IEventStore {
} }
async batchStore(events: IBaseEvent[]): Promise<void> { async batchStore(events: IBaseEvent[]): Promise<void> {
const stopTimer = this.metricTimer('batchStore');
try { try {
await this.db(TABLE).insert( await this.db(TABLE).insert(
events.map((event) => this.eventToDbRow(event)), events.map((event) => this.eventToDbRow(event)),
@ -169,10 +186,13 @@ export class EventStore implements IEventStore {
`Failed to store events: ${JSON.stringify(events)}`, `Failed to store events: ${JSON.stringify(events)}`,
error, error,
); );
} finally {
stopTimer();
} }
} }
async getMaxRevisionId(largerThan: number = 0): Promise<number> { async getMaxRevisionId(largerThan: number = 0): Promise<number> {
const stopTimer = this.metricTimer('getMaxRevisionId');
const row = await this.db(TABLE) const row = await this.db(TABLE)
.max('id') .max('id')
.where((builder) => .where((builder) =>
@ -193,10 +213,12 @@ export class EventStore implements IEventStore {
) )
.andWhere('id', '>=', largerThan) .andWhere('id', '>=', largerThan)
.first(); .first();
stopTimer();
return row?.max ?? 0; return row?.max ?? 0;
} }
async getRevisionRange(start: number, end: number): Promise<IEvent[]> { async getRevisionRange(start: number, end: number): Promise<IEvent[]> {
const stopTimer = this.metricTimer('getRevisionRange');
const query = this.db const query = this.db
.select(EVENT_COLUMNS) .select(EVENT_COLUMNS)
.from(TABLE) .from(TABLE)
@ -246,6 +268,7 @@ export class EventStore implements IEventStore {
} }
async query(operations: IQueryOperations[]): Promise<IEvent[]> { async query(operations: IQueryOperations[]): Promise<IEvent[]> {
const stopTimer = this.metricTimer('query');
try { try {
let query: Knex.QueryBuilder = this.select(); let query: Knex.QueryBuilder = this.select();
@ -271,10 +294,13 @@ export class EventStore implements IEventStore {
return rows.map(this.rowToEvent); return rows.map(this.rowToEvent);
} catch (e) { } catch (e) {
return []; return [];
} finally {
stopTimer();
} }
} }
async queryCount(operations: IQueryOperations[]): Promise<number> { async queryCount(operations: IQueryOperations[]): Promise<number> {
const stopTimer = this.metricTimer('queryCount');
try { try {
let query: Knex.QueryBuilder = this.db.count().from(TABLE); let query: Knex.QueryBuilder = this.db.count().from(TABLE);
@ -300,6 +326,8 @@ export class EventStore implements IEventStore {
return Number.parseInt(queryResult.count || 0); return Number.parseInt(queryResult.count || 0);
} catch (e) { } catch (e) {
return 0; return 0;
} finally {
stopTimer();
} }
} }
@ -355,6 +383,7 @@ export class EventStore implements IEventStore {
} }
async getEvents(query?: Object): Promise<IEvent[]> { async getEvents(query?: Object): Promise<IEvent[]> {
const stopTimer = this.metricTimer('getEvents');
try { try {
let qB = this.db let qB = this.db
.select(EVENT_COLUMNS) .select(EVENT_COLUMNS)
@ -371,6 +400,8 @@ export class EventStore implements IEventStore {
return rows.map(this.rowToEvent); return rows.map(this.rowToEvent);
} catch (err) { } catch (err) {
return []; return [];
} finally {
stopTimer();
} }
} }
@ -379,6 +410,7 @@ export class EventStore implements IEventStore {
queryParams: IQueryParam[], queryParams: IQueryParam[],
options?: { withIp?: boolean }, options?: { withIp?: boolean },
): Promise<IEvent[]> { ): Promise<IEvent[]> {
const stopTimer = this.metricTimer('searchEvents');
const query = this.buildSearchQuery(queryParams, params.query) const query = this.buildSearchQuery(queryParams, params.query)
.select(options?.withIp ? [...EVENT_COLUMNS, 'ip'] : EVENT_COLUMNS) .select(options?.withIp ? [...EVENT_COLUMNS, 'ip'] : EVENT_COLUMNS)
.orderBy([ .orderBy([
@ -396,6 +428,8 @@ export class EventStore implements IEventStore {
); );
} catch (err) { } catch (err) {
return []; return [];
} finally {
stopTimer();
} }
} }
@ -420,6 +454,7 @@ export class EventStore implements IEventStore {
} }
async getEventCreators(): Promise<Array<{ id: number; name: string }>> { async getEventCreators(): Promise<Array<{ id: number; name: string }>> {
const stopTimer = this.metricTimer('getEventCreators');
const query = this.db('events') const query = this.db('events')
.distinctOn('events.created_by_user_id') .distinctOn('events.created_by_user_id')
.leftJoin('users', 'users.id', '=', 'events.created_by_user_id') .leftJoin('users', 'users.id', '=', 'events.created_by_user_id')
@ -437,6 +472,7 @@ export class EventStore implements IEventStore {
]); ]);
const result = await query; const result = await query;
stopTimer();
return result return result
.filter((row: any) => row.name || row.username || row.email) .filter((row: any) => row.name || row.username || row.email)
.map((row: any) => ({ .map((row: any) => ({
@ -448,6 +484,7 @@ export class EventStore implements IEventStore {
async getProjectRecentEventActivity( async getProjectRecentEventActivity(
project: string, project: string,
): Promise<ProjectActivitySchema> { ): Promise<ProjectActivitySchema> {
const stopTimer = this.metricTimer('getProjectRecentEventActivity');
const result = await this.db('events') const result = await this.db('events')
.select( .select(
this.db.raw("TO_CHAR(created_at::date, 'YYYY-MM-DD') AS date"), this.db.raw("TO_CHAR(created_at::date, 'YYYY-MM-DD') AS date"),
@ -462,6 +499,7 @@ export class EventStore implements IEventStore {
.groupBy(this.db.raw("TO_CHAR(created_at::date, 'YYYY-MM-DD')")) .groupBy(this.db.raw("TO_CHAR(created_at::date, 'YYYY-MM-DD')"))
.orderBy('date', 'asc'); .orderBy('date', 'asc');
stopTimer();
return result.map((row) => ({ return result.map((row) => ({
date: row.date, date: row.date,
count: Number(row.count), count: Number(row.count),
@ -531,10 +569,12 @@ export class EventStore implements IEventStore {
} }
async setUnannouncedToAnnounced(): Promise<IEvent[]> { async setUnannouncedToAnnounced(): Promise<IEvent[]> {
const stopTimer = this.metricTimer('setUnannouncedToAnnounced');
const rows = await this.db(TABLE) const rows = await this.db(TABLE)
.update({ announced: true }) .update({ announced: true })
.where('announced', false) .where('announced', false)
.returning(EVENT_COLUMNS); .returning(EVENT_COLUMNS);
stopTimer();
return rows.map(this.rowToEvent); return rows.map(this.rowToEvent);
} }
@ -545,6 +585,7 @@ export class EventStore implements IEventStore {
} }
async setCreatedByUserId(batchSize: number): Promise<number | undefined> { async setCreatedByUserId(batchSize: number): Promise<number | undefined> {
const stopTimer = this.metricTimer('setCreatedByUserId');
const API_TOKEN_TABLE = 'api_tokens'; const API_TOKEN_TABLE = 'api_tokens';
const toUpdate = await this.db(`${TABLE} as e`) const toUpdate = await this.db(`${TABLE} as e`)
@ -592,6 +633,7 @@ export class EventStore implements IEventStore {
}); });
await Promise.all(updatePromises); await Promise.all(updatePromises);
stopTimer();
return toUpdate.length; return toUpdate.length;
} }
} }

View File

@ -19,7 +19,6 @@ import {
} from '../../openapi/index.js'; } from '../../openapi/index.js';
import type { Context } from 'unleash-client'; import type { Context } from 'unleash-client';
import { enrichContextWithIp } from './index.js'; import { enrichContextWithIp } from './index.js';
import { corsOriginMiddleware } from '../../middleware/index.js';
import NotImplementedError from '../../error/not-implemented-error.js'; import NotImplementedError from '../../error/not-implemented-error.js';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import { minutesToMilliseconds } from 'date-fns'; import { minutesToMilliseconds } from 'date-fns';
@ -65,10 +64,6 @@ export default class FrontendAPIController extends Controller {
functionName, functionName,
}); });
// Support CORS requests for the frontend endpoints.
// Preflight requests are handled in `app.ts`.
this.app.use(corsOriginMiddleware(services, config));
this.route({ this.route({
method: 'get', method: 'get',
path: '', path: '',

View File

@ -8,21 +8,27 @@ import {
const requestLogger: (config: IUnleashConfig) => RequestHandler = (config) => { const requestLogger: (config: IUnleashConfig) => RequestHandler = (config) => {
const logger = config.getLogger('HTTP'); const logger = config.getLogger('HTTP');
const enable = config.server.enableRequestLogger; const requestLoggerEnabled = config.server.enableRequestLogger;
const impactMetrics = config.flagResolver.impactMetrics; const impactMetrics = config.flagResolver.impactMetrics;
return (req, res, next) => { return (req, res, next) => {
if (enable) { const impactMetricsEnabled =
config.flagResolver.isEnabled('impactMetrics');
res.on('finish', () => { res.on('finish', () => {
const { pathname } = url.parse(req.originalUrl); if (impactMetricsEnabled && impactMetrics) {
if (res.statusCode >= 400 && res.statusCode < 500) { if (res.statusCode >= 400 && res.statusCode < 500) {
impactMetrics?.incrementCounter(CLIENT_ERROR_COUNT); impactMetrics.incrementCounter(CLIENT_ERROR_COUNT);
} }
if (res.statusCode >= 500) { if (res.statusCode >= 500) {
impactMetrics?.incrementCounter(SERVER_ERROR_COUNT); impactMetrics.incrementCounter(SERVER_ERROR_COUNT);
} }
}
if (requestLoggerEnabled) {
const { pathname } = url.parse(req.originalUrl);
logger.info(`${res.statusCode} ${req.method} ${pathname}`); logger.info(`${res.statusCode} ${req.method} ${pathname}`);
});
} }
});
next(); next();
}; };
}; };