Dashboard / Telemetry — Common cases
Widget figures lag behind real data
The dashboard serves through a TTL-only cache, with no event-driven invalidation. Figures can lag by up to the endpoint's TTL; no realtime promise.
Real-world example: A staff member confirms a booking, then returns to view
BookingStatistics. → Distinguish cache lag from a wrong formula. → 300s cache TTL → figures update within at most 5 minutes; this is correct by-design behavior, not a bug.
Switching BU still shows the old BU's figures
The site selector and every widget must reload by the new BusinessUnitId; requests with an unknown/inactive BU are rejected with NOT_FOUND.
Real-world example: An admin viewing BU "Hà Nội" switches to BU "Đà Nẵng". → Avoid mixing two units' figures. → The dashboard reloads by the new BU; if the BU is deactivated → request rejected
NOT_FOUND.
EstimatedRevenue differs from the invoice total
Revenue only counts TermBooking StatusId=500 (Confirmed) + BillingId not null; 450/475 and bookings without an invoice are not counted.
Real-world example: A booking is just confirmed with
StatusId=500butBillingIdis still null. → Do not miscount estimated revenue. → That booking is not inEstimatedRevenueuntil an invoice is attached.
TotalBookings does not drop when choosing a date range
TotalBookings is cumulative up to toDate, not a count within the range; excludes 100/900, includes both TermBooking and TermBookingOrder.
Real-world example: Range 01/05–15/05 but the BU has 100 bookings before 01/05. → Do not mistake it for the number of new bookings. →
TotalBookingsstill includes those 100 old bookings; for new figures checkNewBookings.
NewBookings missing pending
PendingBookings only comes from TermBookingOrder StatusId=300, not from TermBooking.
Real-world example: There are 2 orders at
StatusId=300but they do not appear inNewBookings. → Check the right source. →NewBookings = NewBookingsFromTermBooking + PendingBookings(from orders, status 300).
OnsiteRange goes negative
OnsiteRange = SignedInRange − SignedOutRange; if SignedOutRange > SignedInRange (data anomaly) the result is negative, with no guard at the application layer.
Real-world example: The onsite widget shows a negative number (−2). → This is a roll-call data anomaly, not a widget bug. → Check the underlying
TermAttendancedata; negative behavior is not defined.
HourlyFlow wrong hour vs operating time
CheckInHour must convert SignInKeyedOn (UTC) to the BU's IANA timezone before extracting the hour; PeakStartHour/PeakEndHour share the logic.
Real-world example: BU timezone
Pacific/Auckland, check-in20:30Z(= 09:30 NZ). → Avoid a half-day offset between UTC and local time. → Hour bucket 9 in BU time; if you see an offset, check that the BU timezone is set correctly.
DST shifts hour buckets
Spring-forward must not create phantom hour buckets; fall-back merges repeated hours consistently per standard timezone rules.
Real-world example: A BU in New Zealand, the spring-forward night is missing 02:00–03:00. → Do not invent time that does not exist. → No check-ins in that window; the chart shows no phantom bucket.
Filtering siteIds drops accounts/attendees
With siteIds → the join switches to INNER JOIN; accounts/attendees not in the selected sites are excluded entirely. A site outside the BU → reject the whole request INVALID_ID.
Real-world example: Account X belongs to the BU but is not assigned to any site; filter
siteIds=[2,3]. → Must not leak into the overview table. → X disappears from the result because of theINNER JOIN; one wrong site → request rejected.
SiteDistribution unchanged when fromDate changes
SiteDistribution returns the cumulative total at toDate; fromDate is accepted but has no effect on the query or cache key.
Real-world example: Change
fromDatefrom 01/01 to 01/03, keeptoDatethe same. → The figure must be the total up to now. → The result does not change because onlytoDatematters; do not assume a cache bug.
RegistrationTrend rejects unknown groupBy
groupBy accepts only day/week/month (case-insensitive).
Real-world example: Call with
groupBy=year. → Avoid undefined grouping. → The request returnsINVALID_PARAMS; usemonthfor an equivalent.
Username not found in telemetry
AuthenticatedUserId is taken from name, fallback email → preferred_username → upn → sub; no matching claim → nothing written (avoids empty values). Classic and workspace tables use different column names for queries.
Real-world example: Querying workspace
AppRequestsbyUserAuthenticatedIdfinds nothing but classic does. → Pick the schema of the table you are querying. → Workspace usesUserAuthenticatedId/Properties.userName; classic usesuser_AuthenticatedId/customDimensions.userName.
Telemetry does not log credentials
The initializer only logs the username; it never logs the bearer token, authorization header, claim set, or credentials.
Real-world example: A failing request carries a token. → Do not leak secrets into telemetry. → Only
userNameappears in the telemetry item; token/header are not stored.
Extra case: today's attendance has no endpoint
IDashboardService.GetTodayAttendanceAsync is implemented but has no HTTP endpoint in DashboardController — currently not callable through the public API.
Real-world example: A dev wants to test "today attendance" over HTTP. → Avoid looking for an endpoint that does not exist. → The method exists in the service but is not exposed; an endpoint must be added first to use it.