Skip to content

feat: Resolve device action errors using blueprint deviceActionMessages#1743

Merged
rjmunro merged 7 commits into
Sofie-Automation:mainfrom
bbc:rjmunro/improve-device-error-notifications
Jun 19, 2026
Merged

feat: Resolve device action errors using blueprint deviceActionMessages#1743
rjmunro merged 7 commits into
Sofie-Automation:mainfrom
bbc:rjmunro/improve-device-error-notifications

Conversation

@rjmunro

@rjmunro rjmunro commented May 11, 2026

Copy link
Copy Markdown
Contributor

About the Contributor

This pull request is posted on behalf of the BBC.

Type of Contribution

Feature

Current Behavior

Device action errors (e.g. HTTP Send failures, CasparCG restart failures) are returned to the UI as raw strings from the Playout Gateway. Blueprint authors have no way to customise these messages.

New Behavior

  • deviceActionMessages added to StudioBlueprintManifest — blueprint authors can provide custom message templates (with {{variable}} interpolation) keyed by action error code string
  • resolveActionResult() resolves structured action errors server-side using the studio blueprint, reusing the StatusMessageResolver infrastructure from PR feat: Status message customisation #1604
  • Wired into executePeripheralDeviceAction so customised messages reach the UI automatically

Example blueprint configuration:

import { HttpSendActionErrorCode } from 'timeline-state-resolver-types'

deviceActionMessages: {
  [HttpSendActionErrorCode.REQUEST_FAILED]: 'HTTP action failed - could not reach {{url}}: {{errorMessage}}',
  [HttpSendActionErrorCode.MISSING_URL]: 'HTTP action not configured - no URL set in the timeline object',
}

This is the companion to PR #1604 which covered device status errors — this PR covers device action execution failures.

Testing Instructions

Requires the TSR stacked PR (on #418) for structured error codes from devices.

  1. Configure deviceActionMessages in a studio blueprint with a custom message for HttpSendActionErrorCode.REQUEST_FAILED
  2. Trigger an HTTP Send action with an unreachable URL
  3. Verify the custom message (with interpolated {{url}} and {{errorMessage}}) appears in the UI notification

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Blueprints can now define per-action error message templates in deviceActionMessages to customize peripheral device action execution error responses. Action execution results from peripheral devices are passed through resolveActionResult, which applies blueprint-defined templates, function-based message generators, or suppresses messages before returning the final ActionExecutionResult to the client.

Changes

Device action error message resolution

Layer / File(s) Summary
Blueprint schema for device action messages
packages/blueprints-integration/src/api/studio.ts
StudioBlueprintManifest adds optional deviceActionMessages field supporting per-action-error-code keys mapped to string templates with variable interpolation, DeviceStatusMessageFunction, or empty string to suppress messages.
Peripheral device action result resolution core
meteor/server/api/peripheralDevice.ts
Introduces getStudioBlueprintManifest(studioId) helper to fetch and evaluate studio blueprints. Implements resolveActionResult(deviceId, result) which for non-Ok TSR results with an error code looks up the device's studio blueprint, resolves matching deviceActionMessages entries via template interpolation or function invocation, and returns an updated ActionExecutionResult or the original result on no-match or error.
Client API action invocation with result resolution
meteor/server/api/client.ts
ServerClientAPIClass.callPeripheralDeviceAction now awaits the underlying peripheral call, captures its ActionExecutionResult, and returns resolveActionResult(deviceId, result) so blueprint message resolution is applied at the API boundary.
Test coverage for action result resolution
meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts
Jest test suite validates resolveActionResult across multiple scenarios: returning Ok results unchanged, preserving results when error code is missing, interpolating message templates, supporting function-based generators, suppressing via empty string, handling no-match and blueprint lookup failures.

Sequence Diagram

sequenceDiagram
  participant Client
  participant callPeripheralDeviceAction
  participant callPeripheralDeviceFunctionOrAction
  participant resolveActionResult
  participant PeripheralDevices
  participant Studios
  participant resolveMessage as StatusMessageResolver

  Client->>callPeripheralDeviceAction: callPeripheralDeviceAction(deviceId, action)
  callPeripheralDeviceAction->>callPeripheralDeviceFunctionOrAction: await call
  callPeripheralDeviceFunctionOrAction-->>callPeripheralDeviceAction: ActionExecutionResult
  callPeripheralDeviceAction->>resolveActionResult: resolveActionResult(deviceId, result)
  
  alt result is Ok or code missing
    resolveActionResult-->>callPeripheralDeviceAction: return result unchanged
  else result has error code
    resolveActionResult->>PeripheralDevices: findOne(deviceId)
    PeripheralDevices-->>resolveActionResult: device with studioId
    resolveActionResult->>Studios: findOne and evaluate blueprint
    Studios-->>resolveActionResult: StudioBlueprintManifest
    resolveActionResult->>resolveMessage: resolve deviceActionMessages[code]
    resolveMessage-->>resolveActionResult: resolved key or null
    alt message suppressed
      resolveActionResult-->>callPeripheralDeviceAction: result with response.key = ''
    else message resolved
      resolveActionResult-->>callPeripheralDeviceAction: result with interpolated response.key
    else no match or error
      resolveActionResult-->>callPeripheralDeviceAction: return result unchanged
    end
  end
  
  callPeripheralDeviceAction-->>Client: resolved ActionExecutionResult
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Sofie-Automation/sofie-core#1604: Introduces StatusMessageResolver and device/system message override mechanics that are directly used by the new resolveActionResult function to apply blueprint-based message overrides.

Suggested reviewers

  • nytamin
  • imaretic
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Resolve device action errors using blueprint deviceActionMessages' directly and clearly describes the main feature added - a new capability to resolve device action errors using blueprint configuration.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the current behavior, new behavior, configuration example, and testing instructions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
meteor/server/api/peripheralDevice.ts 97.32% 3 Missing ⚠️
meteor/server/api/client.ts 33.33% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@rjmunro rjmunro changed the title feat: resolve device action errors using blueprint deviceActionMessages feat: Resolve device action errors using blueprint deviceActionMessages May 12, 2026
@rjmunro rjmunro force-pushed the rjmunro/error-message-customisation branch 3 times, most recently from 692ce19 to f3e9c26 Compare May 18, 2026 00:31
@rjmunro rjmunro force-pushed the rjmunro/improve-device-error-notifications branch 2 times, most recently from 4825591 to ea7c4ac Compare May 18, 2026 11:45
@rjmunro rjmunro marked this pull request as ready for review May 22, 2026 09:11
@Saftret Saftret added the Contribution from BBC Contributions sponsored by BBC (bbc.co.uk) label May 28, 2026
@rjmunro rjmunro changed the base branch from rjmunro/error-message-customisation to main June 2, 2026 08:44
@rjmunro rjmunro force-pushed the rjmunro/improve-device-error-notifications branch from ea7c4ac to d4a6368 Compare June 2, 2026 08:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/blueprints-integration/src/api/studio.ts (1)

138-138: 💤 Low value

Type inconsistency with deviceStatusMessages.

deviceActionMessages includes | undefined in the value union, whereas deviceStatusMessages (line 113) does not. The doc block only describes string templates, functions, and empty-string suppression — it doesn't mention undefined. Adding | undefined to a Record value is redundant for optionality and just diverges from the sibling field. Consider dropping it for consistency (or documenting it intentionally).

♻️ Proposed change
-	deviceActionMessages?: Record<string, string | DeviceStatusMessageFunction | undefined>
+	deviceActionMessages?: Record<string, string | DeviceStatusMessageFunction>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/blueprints-integration/src/api/studio.ts` at line 138, The type for
deviceActionMessages is inconsistent with deviceStatusMessages: remove the
redundant "| undefined" from the Record value union so the property stays
optional via the "?" and the values match deviceStatusMessages; update the
deviceActionMessages declaration to use Record<string, string |
DeviceStatusMessageFunction> and, if the inclusion of undefined was intentional,
instead document that behavior in the docblock for deviceActionMessages to match
the expected semantics.
meteor/server/api/peripheralDevice.ts (1)

209-227: ⚖️ Poor tradeoff

Duplicated device → studio → blueprint → manifest lookup.

This sequence (find device, find studio, find blueprint, evalBlueprint) duplicates the logic in resolveDeviceStatusDetails (Lines 104-124). Consider extracting a small shared helper that returns the resolved StudioBlueprintManifest for a studio/device, so both message-resolution paths stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meteor/server/api/peripheralDevice.ts` around lines 209 - 227, Duplicate
device→studio→blueprint→manifest lookup: extract a small shared helper (e.g.,
resolveStudioBlueprintManifestForDevice or getStudioBlueprintManifest) that
encapsulates the sequence of PeripheralDevices.findOneAsync,
Studios.findOneAsync, Blueprints.findOneAsync and evalBlueprint and returns the
StudioBlueprintManifest (or undefined on failure); replace the inline sequence
in the current code and in resolveDeviceStatusDetails with calls to this helper
so both paths use the same lookup logic and return shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@meteor/server/api/peripheralDevice.ts`:
- Around line 246-255: The branch that returns early when resolved === null
should instead remove/suppress the TSR message so it doesn't reach the UI: in
the function resolveActionResult (the code path that calls
StatusMessageResolver.getDeviceStatusMessage and assigns
resolved/defaultMessage), replace the early return with logic that clears
result.response and any translatable response fields (e.g.
result.responseTranslated and result.translatableResponse or similarly named
properties on result) and leaves other result metadata intact; keep the existing
defaultMessage check unchanged. Ensure you only mutate the response fields (set
to empty string or undefined per local patterns) when resolved === null so the
blueprint suppression takes effect.

---

Nitpick comments:
In `@meteor/server/api/peripheralDevice.ts`:
- Around line 209-227: Duplicate device→studio→blueprint→manifest lookup:
extract a small shared helper (e.g., resolveStudioBlueprintManifestForDevice or
getStudioBlueprintManifest) that encapsulates the sequence of
PeripheralDevices.findOneAsync, Studios.findOneAsync, Blueprints.findOneAsync
and evalBlueprint and returns the StudioBlueprintManifest (or undefined on
failure); replace the inline sequence in the current code and in
resolveDeviceStatusDetails with calls to this helper so both paths use the same
lookup logic and return shape.

In `@packages/blueprints-integration/src/api/studio.ts`:
- Line 138: The type for deviceActionMessages is inconsistent with
deviceStatusMessages: remove the redundant "| undefined" from the Record value
union so the property stays optional via the "?" and the values match
deviceStatusMessages; update the deviceActionMessages declaration to use
Record<string, string | DeviceStatusMessageFunction> and, if the inclusion of
undefined was intentional, instead document that behavior in the docblock for
deviceActionMessages to match the expected semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f6cb12a4-12e7-4a21-8be6-ac050517d0fe

📥 Commits

Reviewing files that changed from the base of the PR and between c580d76 and d4a6368.

📒 Files selected for processing (3)
  • meteor/server/api/client.ts
  • meteor/server/api/peripheralDevice.ts
  • packages/blueprints-integration/src/api/studio.ts

Comment thread meteor/server/api/peripheralDevice.ts
rjmunro added 2 commits June 18, 2026 10:59
Updates to 10.0.0-nightly-main-20260602-133931-c0882da4d.0 which includes
the new code and context fields on ActionExecutionResult (merged via
Sofie-Automation/sofie-timeline-state-resolver#459).
@rjmunro rjmunro force-pushed the rjmunro/improve-device-error-notifications branch from 38283b2 to 62e2966 Compare June 18, 2026 10:05
rjmunro added 3 commits June 18, 2026 12:34
…essage

- Return an empty response key when deviceActionMessages resolves to null
- Update resolveActionResult docstring to document suppression behaviour
- Share studio-to-blueprint manifest lookup between status and action resolution
- Reduces duplicated PeripheralDevices/Studios/Blueprints query chain
…iceStatusMessages

- Remove redundant undefined union member from the Record value type
- Suppression is expressed via empty string, consistent with deviceStatusMessages

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
meteor/server/api/peripheralDevice.ts (1)

222-229: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle child-device studio fallback before returning unchanged results.

At Line 226, resolveActionResult returns early when studioAndConfigId is absent. In the same file, setStatus already handles child devices via parent lookup (Line 414-Line 420). Without the same fallback here, blueprint deviceActionMessages won’t apply to child-device actions.

Suggested fix
-		const device = (await PeripheralDevices.findOneAsync(deviceId, {
-			projection: { name: 1, studioAndConfigId: 1 },
-		})) as Pick<PeripheralDevice, 'name' | 'studioAndConfigId'> | undefined
+		const device = (await PeripheralDevices.findOneAsync(deviceId, {
+			projection: { name: 1, studioAndConfigId: 1, parentDeviceId: 1 },
+		})) as Pick<PeripheralDevice, 'name' | 'studioAndConfigId' | 'parentDeviceId'> | undefined
 
-		if (!device?.studioAndConfigId?.studioId) return result
+		let studioId = device?.studioAndConfigId?.studioId
+		if (!studioId && device?.parentDeviceId) {
+			const parentDevice = (await PeripheralDevices.findOneAsync(device.parentDeviceId, {
+				projection: { studioAndConfigId: 1 },
+			})) as Pick<PeripheralDevice, 'studioAndConfigId'> | undefined
+			studioId = parentDevice?.studioAndConfigId?.studioId
+		}
+		if (!studioId) return result
 
-		const studioBlueprint = await getStudioBlueprintManifest(device.studioAndConfigId.studioId)
+		const studioBlueprint = await getStudioBlueprintManifest(studioId)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meteor/server/api/peripheralDevice.ts` around lines 222 - 229, The
resolveActionResult function returns early when studioAndConfigId is absent, but
it does not handle child devices the way setStatus does with its parent device
lookup logic at lines 414-420. Before returning an unchanged result when
studioAndConfigId is missing, add fallback logic to look up the parent device
(similar to the setStatus parent lookup pattern) to retrieve the studio ID for
child devices, ensuring that blueprint deviceActionMessages will be properly
applied to child-device actions instead of being skipped.
🧹 Nitpick comments (1)
meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts (1)

160-170: ⚡ Quick win

Add a regression test for child-device studio resolution.

Please add a case where the first PeripheralDevices.findOneAsync returns a child device (no studioAndConfigId, with parentDeviceId) and the parent lookup provides the studio. That locks in the intended action-message resolution behavior once the fallback is implemented.

Suggested test shape
+	it('resolves messages for child devices via parent studio', async () => {
+		const parentId = protectString<PeripheralDeviceId>('parent0')
+		jest
+			.spyOn(PeripheralDevices, 'findOneAsync')
+			.mockResolvedValueOnce({
+				name: 'Child Device',
+				parentDeviceId: parentId,
+			} as any)
+			.mockResolvedValueOnce({
+				name: 'Playout Gateway',
+				studioAndConfigId: { studioId, configId: 'config0' },
+			} as any)
+		jest.spyOn(Studios, 'findOneAsync').mockResolvedValue({ blueprintId: 'blueprint0' } as any)
+		jest.spyOn(Blueprints, 'findOneAsync').mockResolvedValue({
+			_id: 'blueprint0',
+			name: 'test',
+			code: '',
+		} as any)
+		mockEvalBlueprint.mockReturnValue({
+			deviceActionMessages: {
+				[ACTION_ERROR_CODE]: 'Failed to trigger graphics at {{url}}: {{errorMessage}}',
+			},
+		} as any)
+
+		const resolved = await resolveActionResult(deviceId, makeErrorResult())
+		expect(resolved.response).toEqual({
+			key: 'Failed to trigger graphics at http://graphics/api: connection refused',
+		})
+	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts`
around lines 160 - 170, Add a new regression test after the existing test case
that verifies studio resolution for child devices. The new test should mock
PeripheralDevices.findOneAsync to return a child device object containing a
parentDeviceId but no studioAndConfigId, then mock a subsequent
PeripheralDevices.findOneAsync call (for parent lookup) to return a parent
device with a studioAndConfigId. Call resolveActionResult with a test result and
verify that the resolved result properly incorporates the studio information
from the parent device, ensuring the fallback behavior is locked in for
child-device studio resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@meteor/server/api/peripheralDevice.ts`:
- Around line 222-229: The resolveActionResult function returns early when
studioAndConfigId is absent, but it does not handle child devices the way
setStatus does with its parent device lookup logic at lines 414-420. Before
returning an unchanged result when studioAndConfigId is missing, add fallback
logic to look up the parent device (similar to the setStatus parent lookup
pattern) to retrieve the studio ID for child devices, ensuring that blueprint
deviceActionMessages will be properly applied to child-device actions instead of
being skipped.

---

Nitpick comments:
In `@meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts`:
- Around line 160-170: Add a new regression test after the existing test case
that verifies studio resolution for child devices. The new test should mock
PeripheralDevices.findOneAsync to return a child device object containing a
parentDeviceId but no studioAndConfigId, then mock a subsequent
PeripheralDevices.findOneAsync call (for parent lookup) to return a parent
device with a studioAndConfigId. Call resolveActionResult with a test result and
verify that the resolved result properly incorporates the studio information
from the parent device, ensuring the fallback behavior is locked in for
child-device studio resolution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0c6e333-2227-41cf-8a4b-e9da95bafe8e

📥 Commits

Reviewing files that changed from the base of the PR and between 62e2966 and b8cf11c.

📒 Files selected for processing (3)
  • meteor/server/api/__tests__/peripheralDevice.resolveActionResult.test.ts
  • meteor/server/api/peripheralDevice.ts
  • packages/blueprints-integration/src/api/studio.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/blueprints-integration/src/api/studio.ts

- Cover success, template interpolation, function messages, and suppression
- Cover missing blueprint, missing studio, and unmatched error codes
@rjmunro rjmunro force-pushed the rjmunro/improve-device-error-notifications branch from b8cf11c to fbabd9c Compare June 18, 2026 12:18
- Fall back to parent device studioAndConfigId when the target device has none
- Matches the existing setStatus lookup pattern for subdevices like casparcg0
@rjmunro rjmunro merged commit c29c1a7 into Sofie-Automation:main Jun 19, 2026
24 checks passed
@rjmunro rjmunro deleted the rjmunro/improve-device-error-notifications branch June 19, 2026 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Contribution from BBC Contributions sponsored by BBC (bbc.co.uk)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants