feat(feedback): capture the browser UTC offset; Filed time in three zones
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
The account time zone defaults to UTC until a player saves a profile, so a report's Filed time could only render in UTC even for a player clearly in another zone. Capture the client's detected "±HH:MM" offset (browser_tz) with each submission and show the Filed time in three zones in the operator console — UTC, the browser offset detected at submit, and the sender's saved profile zone — each shown "N/A" when not known, so the operator can tell what is certainly known from what is merely defaulted. - Thread browser_tz through the fbs envelope (+ Go/TS codegen), the gateway transcode + backend client, and the backend feedback service/store; add the column via migration 00004 (additive, image-rollback-safe). - Fix fmtTimeIn to resolve "±HH:MM" offsets via account.ResolveZone; time.LoadLocation alone silently fell back to UTC for offset zones, which is the second reason a "+02:00" sender showed only UTC. - Update ARCHITECTURE/FUNCTIONAL(+ru) docs and the feedback integration test.
This commit is contained in:
@@ -76,20 +76,22 @@ describe('codec', () => {
|
||||
it('round-trips a feedback submit and decodes state + unread', () => {
|
||||
const att = new Uint8Array([1, 2, 3, 4]);
|
||||
const req = fb.FeedbackSubmitRequest.getRootAsFeedbackSubmitRequest(
|
||||
new ByteBuffer(encodeFeedbackSubmit('please fix', att, 'shot.png', 'ios', 'v1.2.3')),
|
||||
new ByteBuffer(encodeFeedbackSubmit('please fix', att, 'shot.png', 'ios', 'v1.2.3', '+03:00')),
|
||||
);
|
||||
expect(req.body()).toBe('please fix');
|
||||
expect(req.attachmentName()).toBe('shot.png');
|
||||
expect(req.channel()).toBe('ios');
|
||||
expect(req.version()).toBe('v1.2.3');
|
||||
expect(req.browserTz()).toBe('+03:00');
|
||||
expect(Array.from(req.attachmentArray() ?? [])).toEqual([1, 2, 3, 4]);
|
||||
|
||||
// No attachment: the vector is empty.
|
||||
const req2 = fb.FeedbackSubmitRequest.getRootAsFeedbackSubmitRequest(
|
||||
new ByteBuffer(encodeFeedbackSubmit('hi', null, '', 'web', 'dev')),
|
||||
new ByteBuffer(encodeFeedbackSubmit('hi', null, '', 'web', 'dev', '+00:00')),
|
||||
);
|
||||
expect(req2.body()).toBe('hi');
|
||||
expect(req2.version()).toBe('dev');
|
||||
expect(req2.browserTz()).toBe('+00:00');
|
||||
expect(req2.attachmentLength()).toBe(0);
|
||||
|
||||
// State carrying a reply.
|
||||
|
||||
@@ -490,6 +490,7 @@ export function encodeFeedbackSubmit(
|
||||
attachmentName: string,
|
||||
channel: string,
|
||||
version: string,
|
||||
browserTz: string,
|
||||
): Uint8Array {
|
||||
const b = new Builder(256);
|
||||
const bodyOff = b.createString(body);
|
||||
@@ -497,12 +498,14 @@ export function encodeFeedbackSubmit(
|
||||
const nameOff = b.createString(attachmentName);
|
||||
const chOff = b.createString(channel);
|
||||
const verOff = b.createString(version);
|
||||
const tzOff = b.createString(browserTz);
|
||||
fb.FeedbackSubmitRequest.startFeedbackSubmitRequest(b);
|
||||
fb.FeedbackSubmitRequest.addBody(b, bodyOff);
|
||||
if (attOff) fb.FeedbackSubmitRequest.addAttachment(b, attOff);
|
||||
fb.FeedbackSubmitRequest.addAttachmentName(b, nameOff);
|
||||
fb.FeedbackSubmitRequest.addChannel(b, chOff);
|
||||
fb.FeedbackSubmitRequest.addVersion(b, verOff);
|
||||
fb.FeedbackSubmitRequest.addBrowserTz(b, tzOff);
|
||||
return finish(b, fb.FeedbackSubmitRequest.endFeedbackSubmitRequest(b));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createConnectTransport } from '@connectrpc/connect-web';
|
||||
import { Gateway } from '../gen/edge/v1/edge_pb';
|
||||
import { GatewayError, type GatewayClient } from './client';
|
||||
import * as codec from './codec';
|
||||
import { browserOffset } from './profileValidation';
|
||||
import { registerProbe, reportOffline, reportOnline } from './connection.svelte';
|
||||
import { backoffMs, isConnectionCode, retryable, toGatewayError } from './retry';
|
||||
|
||||
@@ -147,9 +148,10 @@ export function createTransport(baseUrl: string): GatewayClient {
|
||||
await exec('chat.read', codec.encodeGameAction(id));
|
||||
},
|
||||
async feedbackSubmit(body, attachment, attachmentName, channel) {
|
||||
// The app build (Vite define) rides with the report so the operator sees which version
|
||||
// it came from; the caller need not pass it.
|
||||
await exec('feedback.submit', codec.encodeFeedbackSubmit(body, attachment, attachmentName, channel, __APP_VERSION__));
|
||||
// The app build (Vite define) and the device's detected UTC offset ride with the report
|
||||
// so the operator sees which version it came from and the local time it was filed; the
|
||||
// caller need not pass them.
|
||||
await exec('feedback.submit', codec.encodeFeedbackSubmit(body, attachment, attachmentName, channel, __APP_VERSION__, browserOffset()));
|
||||
},
|
||||
async feedbackGet() {
|
||||
return codec.decodeFeedbackState(await exec('feedback.get', codec.empty()));
|
||||
|
||||
Reference in New Issue
Block a user