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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class GetMyEnvironments {
}
}

function shortenEnvironmentName(name: string): string {
export function shortenEnvironmentName(name: string): string {
const mapToShotEnvName: Record<EnvironmentEnum, string> = {
[EnvironmentEnum.PRODUCTION]: 'prod',
[EnvironmentEnum.DEVELOPMENT]: 'dev',
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/app/events/dtos/trigger-event-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export class TriggerEventResponseDto {
@IsString()
transactionId?: string;

@ApiProperty({
description: 'Link to the activity feed for this trigger event',
type: String,
required: false,
})
@IsOptional()
@IsString()
activityFeedLink?: string;

@IsOptional()
jobData?: IWorkflowDataDto;
}
5 changes: 1 addition & 4 deletions apps/api/src/app/events/events.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
TriggerRequestCategoryEnum,
UserSessionData,
} from '@novu/shared';
import { v4 as uuidv4 } from 'uuid';
import { PayloadValidationExceptionDto } from '../../error-dto';
import { RequireAuthentication } from '../auth/framework/auth.decorator';
import { ExternalApiAccessible } from '../auth/framework/external-api.decorator';
Expand Down Expand Up @@ -178,8 +177,6 @@ export class EventsController {
@Body() body: TriggerEventToAllRequestDto,
@Req() req: RequestWithReqId
): Promise<TriggerEventResponseDto> {
const transactionId = body.transactionId || uuidv4();

return this.triggerEventToAll.execute(
TriggerEventToAllCommand.create({
userId: user._id,
Expand All @@ -188,7 +185,7 @@ export class EventsController {
identifier: body.name,
payload: body.payload,
tenant: body.tenant,
transactionId,
transactionId: body.transactionId,
overrides: body.overrides || {},
actor: body.actor,
context: body.context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,12 @@ export class ParseEventRequest {
);
}

const activityFeedLink = `${process.env.FRONT_BASE_URL}/env/${command.environmentId}/activity/requests?selectedLogId=${requestId}`;
return {
acknowledged: true,
status: TriggerEventStatusEnum.PROCESSED,
transactionId,
activityFeedLink,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also need to adjust the DTO for the SDK?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent adjust the dto for the sdk for this use case clients
like TriggerEventResponseDto
in addition make sure to pass this new prop activityFeedLink TriggerEventToAll usecase as well.

in handler @post('/trigger/broadcast') remove the transaction generation const transactionId = body.transactionId || uuidv4() and in TriggerEventToAll insead of useing the command transactionId use the one that is returned from parseEventRequest usecase

Copy link
Contributor

@cursor cursor bot Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! I've successfully completed all the requested changes:

Changes Made:

1. Updated TriggerEventResponseDto

Added the activityFeedLink field to the SDK DTO with proper API documentation:

export class TriggerEventResponseDto {
  @ApiProperty({
    description: 'Indicates whether the trigger was acknowledged or not',
    type: Boolean,
  })
  @IsBoolean()
  @IsDefined()
  acknowledged: boolean;

  @ApiProperty({
    description: 'Status of the trigger',
    enum: TriggerEventStatusEnum,
  })
  @IsDefined()
  @IsEnum(TriggerEventStatusEnum)
  status: TriggerEventStatusEnum;

  @ApiProperty({
    description: 'In case of an error, this field will contain the error message(s)',
    type: [String], // Specify that this is an array of strings
    required: false, // Not required since it's optional
  })
  @IsOptional()
  error?: string[];

  @ApiProperty({
    description: 'The returned transaction ID of the trigger',
    type: String,
    required: false,
  })
  @IsOptional()
  @IsString()
  transactionId?: string;

  @ApiProperty({
    description: 'Link to the activity feed for this trigger event',
    type: String,
    required: false,
  })
  @IsOptional()
  @IsString()
  activityFeedLink?: string;

  @IsOptional()
  jobData?: IWorkflowDataDto;
}

2. Updated TriggerEventToAll Usecase

Now captures and returns the complete result from parseEventRequest, including the activityFeedLink:

  public async execute(command: TriggerEventToAllCommand) {
    const result = await this.parseEventRequest.execute(
      ParseEventRequestBroadcastCommand.create({
        userId: command.userId,
        environmentId: command.environmentId,
        organizationId: command.organizationId,
        identifier: command.identifier,
        payload: command.payload || {},
        addressingType: AddressingTypeEnum.BROADCAST,
        transactionId: command.transactionId,
        overrides: command.overrides || {},
        actor: command.actor,
        tenant: command.tenant,
        context: command.context,
        requestCategory: TriggerRequestCategoryEnum.SINGLE,
        bridgeUrl: command.bridgeUrl,
        requestId: command.requestId,
      })
    );

    return result;
  }

3. Updated Broadcast Handler

Removed the transaction ID generation logic and simplified the handler to pass the transactionId directly from the request body:

  async broadcastEventToAll(
    @UserSession() user: UserSessionData,
    @Body() body: TriggerEventToAllRequestDto,
    @Req() req: RequestWithReqId
  ): Promise<TriggerEventResponseDto> {
    return this.triggerEventToAll.execute(
      TriggerEventToAllCommand.create({
        userId: user._id,
        environmentId: user.environmentId,
        organizationId: user.organizationId,
        identifier: body.name,
        payload: body.payload,
        tenant: body.tenant,
        transactionId: body.transactionId,
        overrides: body.overrides || {},
        actor: body.actor,
        context: body.context,
        requestId: req._nvRequestId,
      })
    );
  }

4. Updated TriggerEventToAllCommand

Made transactionId optional so it can be generated by parseEventRequest if not provided:

  @IsString()
  @IsOptional()
  transactionId?: string;

5. Removed Unused Import

Cleaned up the unused uuid import from the controller.

The implementation now follows the same pattern as the regular /trigger endpoint, where parseEventRequest handles transaction ID generation and returns the activity feed link that clients can use for debugging and monitoring.


Open in Cursor Open in Web

Learn more about Cursor Agents

jobData: command.skipQueueInsertion ? jobData : undefined,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export class TriggerEventToAllCommand extends EnvironmentWithUserCommand {
payload: any;

@IsString()
@IsDefined()
transactionId: string;
@IsOptional()
transactionId?: string;

@IsObject()
@IsOptional()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class TriggerEventToAll {
constructor(private parseEventRequest: ParseEventRequest) {}

public async execute(command: TriggerEventToAllCommand) {
await this.parseEventRequest.execute(
const result = await this.parseEventRequest.execute(
ParseEventRequestBroadcastCommand.create({
userId: command.userId,
environmentId: command.environmentId,
Expand All @@ -27,10 +27,6 @@ export class TriggerEventToAll {
})
);

return {
acknowledged: true,
status: TriggerEventStatusEnum.PROCESSED,
transactionId: command.transactionId,
};
return result;
}
}
4 changes: 2 additions & 2 deletions apps/api/src/app/shared/helpers/build-slug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ const SLUG_DELIMITER = '_';
* Builds a slug for a step based on the step name, the short prefix and the internal ID.
* @returns The slug for the entity, example: slug: "workflow-name_wf_AbC1Xyz9KlmNOpQr"
*/
export function buildSlug(entityName: string, shortIsPrefix: ShortIsPrefixEnum, internalId: string): Slug {
return `${slugify(entityName)}${SLUG_DELIMITER}${shortIsPrefix}${encodeBase62(internalId)}`;
export function buildSlug(entityName: string, shortIdPrefix: ShortIsPrefixEnum, internalId: string): Slug {
return `${slugify(entityName)}${SLUG_DELIMITER}${shortIdPrefix}${encodeBase62(internalId)}`;
}
16 changes: 10 additions & 6 deletions apps/dashboard/src/context/environment/environment-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ function selectEnvironment(
let environment: IEnvironment | undefined;

// Find the environment based on the current user's last environment
// Support both slug and _id
if (selectedEnvironmentSlug) {
environment = environments.find((env) => env.slug === selectedEnvironmentSlug);
environment = environments.find(
(env) => env.slug === selectedEnvironmentSlug || env._id === selectedEnvironmentSlug
);
}

// If no environment slug in URL, try to load the last selected environment from storage
Expand Down Expand Up @@ -49,7 +52,8 @@ function selectEnvironment(
export function EnvironmentProvider({ children }: { children: React.ReactNode }) {
const { currentOrganization } = useAuth();
const navigate = useNavigate();
const { pathname } = useLocation();
const location = useLocation();
const { pathname, search, hash } = location;
const { environmentSlug: paramsEnvironmentSlug } = useParams<{ environmentSlug?: string }>();
const [currentEnvironment, setCurrentEnvironment] = useState<IEnvironment>();

Expand All @@ -58,7 +62,8 @@ export function EnvironmentProvider({ children }: { children: React.ReactNode })
const selectedEnvironment = selectEnvironment(allEnvironments, environmentSlug, currentOrganization?._id);
setCurrentEnvironment(selectedEnvironment);
const newEnvironmentSlug = selectedEnvironment.slug;
const isNewEnvironmentDifferent = paramsEnvironmentSlug !== selectedEnvironment.slug;
const isNewEnvironmentDifferent =
paramsEnvironmentSlug !== selectedEnvironment.slug && paramsEnvironmentSlug !== selectedEnvironment._id;

// Save the selected environment to localStorage for persistence
if (currentOrganization?._id && newEnvironmentSlug) {
Expand All @@ -70,14 +75,13 @@ export function EnvironmentProvider({ children }: { children: React.ReactNode })
}

if (pathname === ROUTES.ROOT || pathname === ROUTES.ENV || pathname === `${ROUTES.ENV}/`) {
// TODO: check if this ROUTES is correct
navigate(buildRoute(ROUTES.WORKFLOWS, { environmentSlug: newEnvironmentSlug ?? '' }));
} else if (pathname.includes(ROUTES.ENV) && isNewEnvironmentDifferent) {
const newPath = pathname.replace(/\/env\/[^/]+(\/|$)/, `${ROUTES.ENV}/${newEnvironmentSlug}$1`);
navigate(newPath);
navigate(`${newPath}${search}${hash}`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

someone can think of any issues with preserving the search and hash here? today, we were ignoring it and redirecting to the base url

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activity feed filters are saved in the URL, and on the env switch might be breaking or not work

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here is what I am talking about, the URL query params are preserved, but shouldn't be:

Screen.Recording.2025-12-18.at.16.49.49.mov

}
},
[navigate, pathname, paramsEnvironmentSlug, currentOrganization?._id]
[navigate, pathname, search, hash, paramsEnvironmentSlug, currentOrganization?._id]
);

const { environments, areEnvironmentsInitialLoading } = useFetchEnvironments({
Expand Down
Loading