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
39 changes: 39 additions & 0 deletions non production apps/EscapeRoomApp/Docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Changelog

All notable changes to the BCTalent.EscapeRoom framework are documented here.

---

## [1.4.1.0] - 2026-09-14

### Fixed

- **Two rooms could be opened / venue closed prematurely** — `UpdateStatus()` on an already *Completed* room re-ran `OpenNextRoom()` unconditionally. Because that procedure looked for the next *Locked* room, it skipped the room that was already in progress and opened the one after it; once no locked room was left it stopped the venue even though rooms were still in progress. Pressing "Update Status", "Get Hint" or "Solve" on a completed room card (reachable from the room list and from the task-completed notification) triggered it.
- `OpenNextRoom()` is now idempotent: it exits when a later room is already InProgress, and closes the venue only via `CloseVenueIfCompleted()` (all rooms Completed).
- Room, task and venue status transitions re-read the row under an update lock and re-check the status before modifying, closing the race where the same completion ran in several sessions at once (e.g. event subscribers fired from the concurrency simulations' background sessions).
- Room/venue state is committed before the completion image is shown, so an interrupted session can no longer leave the next room locked.
- Renamed a local variable named `Key` in codeunit 73925 "Escape Room Telemetry" (reserved word in current AL compilers, blocked the build).

### Changed

- `"Escape Room Venue".Stop()` and `CloseVenueIfCompleted()` now return `Boolean` (true when the venue got completed by that call) and no longer show the completion image themselves; `"Escape Room".Stop()` shows it.

---

## [1.4.0.0] - 2026-06-10

### Added

- **Custom Telemetry Events API** — New public `LogCustomEvent` overloads on codeunit 73925 "Escape Room Telemetry" allow room extension apps to emit custom scoring and diagnostic events.
- Task-scoped and room-scoped variants
- Optional `ExtraDimensions` parameter for caller-provided custom dimensions
- Score clamped to -5..+5 to keep leaderboards balanced
- All custom events use the fixed event name `EscapeRoomCustomEvent` with `EventSource = Custom` dimension
- **Custom Events Overview** KQL query in `LeaderboardQueries.kql` for facilitator auditing
- All scoring KQL queries and dashboard tiles now include `EscapeRoomCustomEvent`

---

## [1.3.x] - Previous releases

Initial framework with seven built-in telemetry events, interface-based venue/room/task system, leaderboard KQL queries, and Azure Data Explorer dashboard.
48 changes: 47 additions & 1 deletion non production apps/EscapeRoomApp/Docs/Dev/API-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,52 @@ procedure UpdateStatus()

---

## Telemetry API — Codeunit 73925 "Escape Room Telemetry"

### Built-in Event Procedures

```al
procedure LogFinishedTask(var Task: Record "Escape Room Task")
procedure LogHintRequested(var Task: Record "Escape Room Task")
procedure LogSolutionRequested(var Room: record "Escape Room")
procedure LogRoomStarted(var Room: Record "Escape Room")
procedure LogRoomCompleted(var Room: Record "Escape Room")
procedure LogVenueCompleted(var Venue: Record "Escape Room Venue")
procedure LogNotification(NotificationText: Text)
```

### Custom Event Procedures

```al
/// Task-scoped custom event (standard task/room/venue dimensions added automatically)
procedure LogCustomEvent(var EscapeRoomTask: Record "Escape Room Task"; EventId: Text; EventMessage: Text; ScorePoints: Integer)

/// Room-scoped custom event (standard room/venue dimensions added automatically)
procedure LogCustomEvent(var EscapeRoom: Record "Escape Room"; EventId: Text; EventMessage: Text; ScorePoints: Integer)

/// Task-scoped custom event with additional caller-provided dimensions
procedure LogCustomEvent(var EscapeRoomTask: Record "Escape Room Task"; EventId: Text; EventMessage: Text; ScorePoints: Integer; ExtraDimensions: Dictionary of [Text, Text])

/// Room-scoped custom event with additional caller-provided dimensions
procedure LogCustomEvent(var EscapeRoom: Record "Escape Room"; EventId: Text; EventMessage: Text; ScorePoints: Integer; ExtraDimensions: Dictionary of [Text, Text])
```

**Parameters:**
- `EventId` — Caller-defined identifier (max 80 chars, trimmed; empty defaults to `Unspecified`). Recommend prefix with venue id, e.g. `DEV1-EasterEggFound`.
- `EventMessage` — Human-readable message logged to Application Insights.
- `ScorePoints` — Points added to leaderboard, clamped to -5..+5.
- `ExtraDimensions` — Optional additional dimensions. Keys that conflict with standard dimensions are silently skipped.

**Behavior:**
- Application Insights event name is always `EscapeRoomCustomEvent`.
- Adds `EventId` and `EventSource = Custom` dimensions automatically.
- Standard dimensions (VenueId, VenueName, RoomName, TaskName, ScorePoints, etc.) cannot be overwritten by caller.
- Never throws — defensive against empty records.

See [Telemetry Integration](../Framework/Telemetry-Integration.md#custom-events-for-room-developers) for full documentation.

---

## Backward Compatibility Guarantee

**Framework Version 1.x Promise:**
Expand All @@ -553,4 +599,4 @@ procedure UpdateStatus()

---

**Last Updated:** January 7, 2026
**Last Updated:** June 10, 2026
23 changes: 16 additions & 7 deletions non production apps/EscapeRoomApp/Docs/Framework/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,11 @@ codeunit 50103 "My Task Validation" implements "IEscape Room Task Validation"
9. Telemetry tracks all events for leaderboard

**Room Navigation:**
- `OpenNextRoom()` procedure finds next room by sequence
- Sets current key and ascending order for proper ordering
- Only InProgress or NotStarted rooms can be opened
- `OpenNextRoom()` opens the first *Locked* room with a higher sequence than the room that was just completed
- It is idempotent: when a later room is already *InProgress* it does nothing, so calling it twice (concurrent sessions, or "Update Status" pressed on an already completed room) can never open a second room
- When no later room is left, it calls `CloseVenueIfCompleted()`, which closes the venue only when *every* room is Completed
- All status transitions (`Start()`, `Stop()` on rooms and tasks, `Stop()` on the venue) re-read the row under an update lock (`ReadIsolation = UpdLock`) and re-check the status before modifying, so two sessions completing the same task/room serialize instead of both performing the transition
- State transitions are committed *before* the completion image is shown, so an interrupted or UI-less (background) session cannot leave the next room locked

---

Expand Down Expand Up @@ -398,12 +400,19 @@ procedure Start()

```al
procedure UpdateStatus()
// Checks task completion and updates room status
// Re-validates open tasks and completes the room when none remain
// On an already completed room it only runs the (idempotent) OpenNextRoom() recovery

procedure CloseRoomIfCompleted()
// Sets room to Completed when all tasks done
// Opens next room automatically
// Logs telemetry for room completion
// Completes the room when all tasks are done (calls Stop())

procedure Stop()
// Locks the row, sets Completed, commits, logs telemetry,
// opens the next room (or closes the venue), then shows the completion image(s)

internal procedure OpenNextRoom() VenueCompleted: Boolean
// Opens the next Locked room; no-op when a later room is InProgress
// Returns true when the venue got completed by this call
```

### Escape Room Task Table Procedures
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,22 @@ Once your venue is created:

---

## Custom Scoring with Telemetry

Room apps can emit custom telemetry events for bonus challenges, easter eggs, or penalties using the `LogCustomEvent` overloads on codeunit 73925 "Escape Room Telemetry". Custom events carry the same standard dimensions as built-in events and participate in the leaderboard scoring pipeline (score clamped to -5..+5).

```al
var
EscapeRoomTelemetry: Codeunit "Escape Room Telemetry";
begin
EscapeRoomTelemetry.LogCustomEvent(Task, 'DEV1-BonusChallenge', 'Participant completed the bonus challenge.', 2);
end;
```

See [Telemetry Integration - Custom Events](Telemetry-Integration.md#custom-events-for-room-developers) for the full API reference, clamping rules, and EventId conventions.

---

## Related Documentation

- [Architecture Overview](Architecture.md) - Framework design patterns
Expand All @@ -1020,4 +1036,4 @@ Once your venue is created:

---

**Last Updated:** January 7, 2026
**Last Updated:** June 10, 2026
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,24 @@ The framework logs seven distinct event types:

---

#### 8. **EscapeRoomCustomEvent**

**When:** Room extension app calls `LogCustomEvent(...)` to emit a custom scoring or diagnostic event
**Score Impact:** -5 to +5 points (caller-defined, clamped)
**Custom Dimensions:**
- VenueId, VenueName
- PartnerName, FullName
- RoomName
- TaskName (when task-scoped)
- ScorePoints (clamped to -5..+5)
- EventId (caller-provided identifier, max 80 chars)
- EventSource: `Custom`
- Any additional caller-provided dimensions (cannot overwrite standard keys)

**Logged By:** `LogCustomEvent(...)` overloads

---

## Scoring System

### Point Values
Expand All @@ -122,6 +140,7 @@ The framework logs seven distinct event types:
| Request hint | **-1** | EscapeRoomHintRequested |
| View solution | **-3** | EscapeRoomSolutionRequested |
| Start room | **0** | EscapeRoomStarted |
| Custom event | **-5..+5** (caller-defined) | EscapeRoomCustomEvent |

### Scoring Examples

Expand Down Expand Up @@ -159,6 +178,10 @@ procedure LogRoomStarted(var Room: Record "Escape Room")
procedure LogRoomCompleted(var Room: Record "Escape Room")
procedure LogVenueCompleted(var Venue: Record "Escape Room Venue")
procedure LogNotification(NotificationText: Text)
procedure LogCustomEvent(var EscapeRoomTask: Record "Escape Room Task"; EventId: Text; EventMessage: Text; ScorePoints: Integer)
procedure LogCustomEvent(var EscapeRoom: Record "Escape Room"; EventId: Text; EventMessage: Text; ScorePoints: Integer)
procedure LogCustomEvent(var EscapeRoomTask: Record "Escape Room Task"; EventId: Text; EventMessage: Text; ScorePoints: Integer; ExtraDimensions: Dictionary of [Text, Text])
procedure LogCustomEvent(var EscapeRoom: Record "Escape Room"; EventId: Text; EventMessage: Text; ScorePoints: Integer; ExtraDimensions: Dictionary of [Text, Text])
```

**Internal Procedures:**
Expand Down Expand Up @@ -221,6 +244,54 @@ Every telemetry event includes rich context:

---

## Custom Events (for room developers)

Room extension apps can emit custom telemetry events using the `LogCustomEvent` overloads on codeunit 73925. Custom events participate in the standard leaderboard scoring pipeline.

### API Signatures

```al
// Task-scoped (includes task + room + venue dimensions)
procedure LogCustomEvent(var EscapeRoomTask: Record "Escape Room Task"; EventId: Text; EventMessage: Text; ScorePoints: Integer)
procedure LogCustomEvent(var EscapeRoomTask: Record "Escape Room Task"; EventId: Text; EventMessage: Text; ScorePoints: Integer; ExtraDimensions: Dictionary of [Text, Text])

// Room-scoped (includes room + venue dimensions)
procedure LogCustomEvent(var EscapeRoom: Record "Escape Room"; EventId: Text; EventMessage: Text; ScorePoints: Integer)
procedure LogCustomEvent(var EscapeRoom: Record "Escape Room"; EventId: Text; EventMessage: Text; ScorePoints: Integer; ExtraDimensions: Dictionary of [Text, Text])
```

### Usage Example

```al
// Award a bonus point when participant discovers an easter egg
procedure OnEasterEggFound(var Task: Record "Escape Room Task")
var
EscapeRoomTelemetry: Codeunit "Escape Room Telemetry";
begin
EscapeRoomTelemetry.LogCustomEvent(
Task,
'DEV1-EasterEggFound',
'Participant discovered the hidden easter egg in Room 3.',
2 // +2 bonus points
);
end;
```

### Rules

- **Score clamping:** `ScorePoints` is clamped to **-5..+5**. Values outside this range are silently clamped to the nearest bound.
- **EventId convention:** Prefix with your venue/app id, e.g. `DEV1-EasterEggFound`. Max 80 characters; empty defaults to `Unspecified`.
- **Event name:** All custom events use the fixed Application Insights message `EscapeRoomCustomEvent`. The caller's `EventId` is a custom dimension — not the event name.
- **EventSource dimension:** Always set to `Custom` to distinguish from built-in framework events.
- **No overwriting standard dimensions:** Caller-provided `ExtraDimensions` cannot overwrite standard keys (`VenueId`, `VenueName`, `RoomName`, `TaskName`, `ScorePoints`, `EventId`, `EventSource`, etc.). Conflicting keys are silently skipped.
- **Never throws:** The telemetry call is defensive — empty records produce empty dimension values, same as built-in behavior.

### Leaderboard Impact

Custom events flow into the standard leaderboard queries. The `ScorePoints` dimension is summed alongside all other scoring events. Use the "Custom Events Overview" query in `LeaderboardQueries.kql` to audit custom scoring during events.

---

## Leaderboard Queries

### KQL Queries
Expand Down Expand Up @@ -446,4 +517,4 @@ end;

---

**Last Updated:** January 7, 2026
**Last Updated:** June 10, 2026
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ EscapeRoomData
VenuesCompleted = countif(eventId == "ALEscapeRoomVenueCompleted"),
HintsUsed = countif(eventId == "ALEscapeRoomHintRequested"),
SolutionsUsed = countif(eventId == "ALEscapeRoomSolutionRequested"),
CustomEvents = countif(eventId == "ALEscapeRoomCustomEvent"),
LastActivity = max(taskStopDateTime),
FastestVenueCompletion = min(VenueCompletionTimeMinutes)
by Attendee, userId, Partner, environmentName
Expand Down Expand Up @@ -83,12 +84,14 @@ EscapeRoomData
VenueBonusPoints = sumif(Score, eventId == "ALEscapeRoomVenueCompleted"),
HintPenalty = sumif(Score, eventId == "ALEscapeRoomHintRequested"),
SolutionPenalty = sumif(Score, eventId == "ALEscapeRoomSolutionRequested"),
CustomEventPoints = sumif(Score, eventId == "ALEscapeRoomCustomEvent"),
TotalScore = sum(Score),
TasksCompleted = countif(eventId == "ALEscapeRoomTaskFinished"),
RoomsCompleted = countif(eventId == "ALEscapeRoomCompleted"),
VenuesCompleted = countif(eventId == "ALEscapeRoomVenueCompleted"),
HintsUsed = countif(eventId == "ALEscapeRoomHintRequested"),
SolutionsUsed = countif(eventId == "ALEscapeRoomSolutionRequested"),
CustomEvents = countif(eventId == "ALEscapeRoomCustomEvent"),
FastestVenueCompletion = min(VenueCompletionTimeMinutes)
by Attendee, Partner, environmentName
| extend
Expand All @@ -107,11 +110,13 @@ EscapeRoomData
VenueBonusPoints,
HintPenalty,
SolutionPenalty,
CustomEventPoints,
ScoreEfficiency,
CompletionTimeMinutes,
TasksCompleted,
HintsUsed,
SolutionsUsed
SolutionsUsed,
CustomEvents
| order by TotalScore desc, CompletionTimeMinutes asc nulls last

// =================================================================
Expand Down Expand Up @@ -139,4 +144,22 @@ EscapeRoomData
CompletionTimeMinutes,
FinalScore,
CompletionDate
| order by CompletionTimeMinutes asc
| order by CompletionTimeMinutes asc

// =================================================================
// Query 4: Custom Events Overview
// =================================================================
EscapeRoomData
| where eventId == "ALEscapeRoomCustomEvent"
| extend
CustomEventId = tostring(customDimensions.alEventId),
EventSource = tostring(customDimensions.alEventSource)
| project
timestamp,
Attendee,
Partner,
venueId,
roomName,
CustomEventId,
Score
| order by timestamp desc
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ The queries work with these primary event types:
- **`ALEscapeRoomHintRequested`**: Hint usage with penalty
- **`ALEscapeRoomSolutionRequested`**: Solution usage with penalty
- **`ALEscapeRoomStarted`**: Room/venue start tracking
- **`ALEscapeRoomCustomEvent`**: Custom events emitted by room extensions (score -5..+5)

### Custom Events & Scoring

Room extension apps can emit custom telemetry events that participate in leaderboard scoring. These events use the `EscapeRoomCustomEvent` event name and carry a caller-defined score (clamped to -5..+5). Use the **"Custom Events Overview"** query in `LeaderboardQueries.kql` to audit custom scoring during an event — it lists timestamp, attendee, partner, venue, room, EventId, and ScorePoints for all custom events.

### Calculated Fields
- **`durationMinutes`**: Calculated from start/stop timestamps
Expand Down
Loading