feat(error_reporting): integrate error reporting

This commit is contained in:
hexxa 2021-12-30 10:33:13 +08:00 committed by Hexxa
parent 9a7cfcb097
commit 711a3a874f
9 changed files with 86 additions and 57 deletions

View file

@ -55,9 +55,9 @@ func (cl *SettingsClient) SetClientCfg(cfg *sitestore.ClientConfig, token *http.
End() End()
} }
func (cl *SettingsClient) ReportError(report *settings.ClientErrorReport, token *http.Cookie) (*http.Response, string, []error) { func (cl *SettingsClient) ReportErrors(reports *settings.ClientErrorReports, token *http.Cookie) (*http.Response, string, []error) {
return cl.r.Post(cl.url("/v1/settings/errors")). return cl.r.Post(cl.url("/v1/settings/errors")).
AddCookie(token). AddCookie(token).
Send(report). Send(reports).
End() End()
} }

View file

@ -1,4 +1,5 @@
import axios, { AxiosRequestConfig } from "axios"; import axios, { AxiosRequestConfig } from "axios";
import { List } from "immutable";
export const defaultTimeout = 10000; export const defaultTimeout = 10000;
export const userIDParam = "uid"; export const userIDParam = "uid";
@ -88,6 +89,11 @@ export interface ClientConfig {
bg: BgConfig; bg: BgConfig;
} }
export interface ClientErrorReport {
report: string;
version: string;
}
export interface IUsersClient { export interface IUsersClient {
login: ( login: (
user: string, user: string,
@ -139,7 +145,7 @@ export interface ISettingsClient {
health: () => Promise<Response>; health: () => Promise<Response>;
getClientCfg: () => Promise<Response>; getClientCfg: () => Promise<Response>;
setClientCfg: (cfg: ClientConfig) => Promise<Response>; setClientCfg: (cfg: ClientConfig) => Promise<Response>;
reportError: (content: string, version: string) => Promise<Response>; reportErrors: (reports: List<ClientErrorReport>) => Promise<Response>;
} }
export interface Response<T = any> { export interface Response<T = any> {

View file

@ -1,6 +1,7 @@
import { BaseClient, Response, userIDParam, Quota } from "."; import { List } from "immutable";
import { ClientConfig } from "./"; import { BaseClient, Response, userIDParam, Quota } from ".";
import { ClientConfig, ClientErrorReport } from "./";
export class SettingsClient extends BaseClient { export class SettingsClient extends BaseClient {
constructor(url: string) { constructor(url: string) {
@ -31,13 +32,12 @@ export class SettingsClient extends BaseClient {
}); });
}; };
reportError = (content: string, version: string): Promise<Response> => { reportErrors = (reports: List<ClientErrorReport>): Promise<Response> => {
return this.do({ return this.do({
method: "post", method: "post",
url: `${this.url}/v1/settings/errors`, url: `${this.url}/v1/settings/errors`,
data: { data: {
content, reports: reports.toArray(),
version,
}, },
}); });
}; };

View file

@ -7,7 +7,7 @@ export interface SettingsClientResps {
healthMockResp?: Response; healthMockResp?: Response;
setClientCfgMockResp?: Response; setClientCfgMockResp?: Response;
getClientCfgMockResp?: Response<ClientConfigMsg>; getClientCfgMockResp?: Response<ClientConfigMsg>;
reportErrorResp?: Response; reportErrorsResp?: Response;
} }
export const resps = { export const resps = {
@ -29,7 +29,7 @@ export const resps = {
}, },
}, },
}, },
reportErrorResp: { reportErrorsResp: {
status: 200, status: 200,
statusText: "", statusText: "",
data: {}, data: {},
@ -66,7 +66,7 @@ export class MockSettingsClient {
return this.wrapPromise(this.resps.getClientCfgMockResp); return this.wrapPromise(this.resps.getClientCfgMockResp);
}; };
reportError = (): Promise<Response> => { reportErrors = (): Promise<Response> => {
return this.wrapPromise(this.resps.reportErrorResp); return this.wrapPromise(this.resps.reportErrorsResp);
}; };
} }

View file

@ -1,4 +1,4 @@
import { Map } from "immutable"; import { Map, List } from "immutable";
import { sha1 } from "object-hash"; import { sha1 } from "object-hash";
import { ILocalStorage, Storage } from "./localstorage"; import { ILocalStorage, Storage } from "./localstorage";
@ -6,13 +6,12 @@ import { ISettingsClient } from "../client";
import { SettingsClient } from "../client/settings"; import { SettingsClient } from "../client/settings";
import { ICoreState } from "../components/core_state"; import { ICoreState } from "../components/core_state";
import { updater } from "../components/state_updater"; import { updater } from "../components/state_updater";
import { alertMsg } from "./env"; import { ClientErrorReport } from "../client";
const errorVer = "0.0.1"; const errorVer = "0.0.1";
const cookieKeyClErrs = "qs_cli_errs"; const cookieKeyClErrs = "qs_cli_errs";
export interface ClientErrorV001 { export interface ClientErrorV001 {
version: string;
error: string; error: string;
timestamp: string; timestamp: string;
state: ICoreState; state: ICoreState;
@ -72,7 +71,6 @@ export class SimpleErrorLogger {
try { try {
const sign = this.getErrorSign(msg); const sign = this.getErrorSign(msg);
const clientErr: ClientErrorV001 = { const clientErr: ClientErrorV001 = {
version: errorVer,
error: msg, error: msg,
timestamp: `${Date.now()}`, timestamp: `${Date.now()}`,
state: updater().props, state: updater().props,
@ -92,16 +90,21 @@ export class SimpleErrorLogger {
report = async (): Promise<null | Error> => { report = async (): Promise<null | Error> => {
try { try {
const errs = this.readErrs(); const errs = this.readErrs();
let reports = List<ClientErrorReport>();
for (let sign of errs.keySeq().toArray()) { for (let sign of errs.keySeq().toArray()) {
const err = errs.get(sign); const errObj = errs.get(sign);
const resp = await this.client.reportError(sign, JSON.stringify(err)); reports = reports.push({
if (resp.status !== 200) { report: JSON.stringify(errObj),
return Error(`failed to report error: ${resp.data}`); version: errorVer,
} });
} }
this.truncate(); const resp = await this.client.reportErrors(reports);
if (resp.status !== 200) {
return Error(`failed to report error: ${resp.data}`);
} else {
this.truncate();
}
} catch (e: any) { } catch (e: any) {
return Error(e); return Error(e);
} }

View file

@ -179,6 +179,32 @@ export class PaneSettings extends React.Component<Props, State, {}> {
render() { render() {
const errRows = this.prepareErrorRows(); const errRows = this.prepareErrorRows();
const errorReportPane =
errRows.size > 0 ? (
<Container>
<Flexbox
children={List([
<h5 className="pane-title">
{this.props.msg.pkg.get("error.report.title")}
</h5>,
<span>
<button className="margin-r-m" onClick={this.reportErrors}>
{this.props.msg.pkg.get("op.submit")}
</button>
<button onClick={this.truncateErrors}>
{this.props.msg.pkg.get("op.truncate")}
</button>
</span>,
])}
childrenStyles={List([{}, { justifyContent: "flex-end" }])}
/>
<div className="hr"></div>
<Rows rows={errRows} sortKeys={List([])} />
</Container>
) : null;
return ( return (
<div id="pane-settings"> <div id="pane-settings">
@ -415,29 +441,7 @@ export class PaneSettings extends React.Component<Props, State, {}> {
</div> </div>
</Container> </Container>
<Container> {errorReportPane}
<Flexbox
children={List([
<h5 className="pane-title">
{this.props.msg.pkg.get("error.report.title")}
</h5>,
<span>
<button className="margin-r-m" onClick={this.reportErrors}>
{this.props.msg.pkg.get("op.submit")}
</button>
<button onClick={this.truncateErrors}>
{this.props.msg.pkg.get("op.truncate")}
</button>
</span>,
])}
childrenStyles={List([{}, { justifyContent: "flex-end" }])}
/>
<div className="hr"></div>
<Rows rows={errRows} sortKeys={List([])} />
</Container>
{/* <div className="hr"></div> {/* <div className="hr"></div>
<div> <div>

View file

@ -83,14 +83,20 @@ type ClientErrorReport struct {
Version string `json:"version"` Version string `json:"version"`
} }
func (h *SettingsSvc) ReportError(c *gin.Context) { type ClientErrorReports struct {
Reports []*ClientErrorReport `json:"reports"`
}
func (h *SettingsSvc) ReportErrors(c *gin.Context) {
var err error var err error
req := &ClientErrorReport{} req := &ClientErrorReports{}
if err = c.ShouldBindJSON(&req); err != nil { if err = c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 400, err)) c.JSON(q.ErrResp(c, 400, err))
return return
} }
h.deps.Log().Errorf("version:%s,error:%s", req.Version, req.Report) for _, report := range req.Reports {
h.deps.Log().Errorf("version:%s,error:%s", report.Version, report.Report)
}
c.JSON(q.Resp(200)) c.JSON(q.Resp(200))
} }

View file

@ -304,7 +304,7 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E
settingsAPI.OPTIONS("/health", settingsSvc.Health) settingsAPI.OPTIONS("/health", settingsSvc.Health)
settingsAPI.GET("/client", settingsSvc.GetClientCfg) settingsAPI.GET("/client", settingsSvc.GetClientCfg)
settingsAPI.PATCH("/client", settingsSvc.SetClientCfg) settingsAPI.PATCH("/client", settingsSvc.SetClientCfg)
settingsAPI.POST("/errors", settingsSvc.ReportError) settingsAPI.POST("/errors", settingsSvc.ReportErrors)
return router, nil return router, nil
} }

View file

@ -126,15 +126,22 @@ func TestSettingsHandlers(t *testing.T) {
} }
}) })
t.Run("ReportError", func(t *testing.T) { t.Run("ReportErrors", func(t *testing.T) {
settingsCl := client.NewSettingsClient(addr) settingsCl := client.NewSettingsClient(addr)
reportContent := `{state: "{}", error: "empty state"}` reports := &settings.ClientErrorReports{
report := &settings.ClientErrorReport{ Reports: []*settings.ClientErrorReport{
Report: reportContent, &settings.ClientErrorReport{
Version: "0.0.1", Report: `{state: "{}", error: "empty state1"}`,
Version: "0.0.1",
},
&settings.ClientErrorReport{
Report: `{state: "{}", error: "empty state2"}`,
Version: "0.0.1",
},
},
} }
reportResp, _, errs := settingsCl.ReportError(report, adminToken) reportResp, _, errs := settingsCl.ReportErrors(reports, adminToken)
if len(errs) > 0 { if len(errs) > 0 {
t.Fatal(errs) t.Fatal(errs)
} else if reportResp.StatusCode != 200 { } else if reportResp.StatusCode != 200 {
@ -154,7 +161,10 @@ func TestSettingsHandlers(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !strings.Contains(string(content), `"msg":"version:0.0.1,error:{state: \"{}\", error: \"empty state\"}"`) { if !strings.Contains(string(content), `"msg":"version:0.0.1,error:{state: \"{}\", error: \"empty state1\"}"`) {
t.Fatalf("log does not contain error: %s", content)
}
if !strings.Contains(string(content), `"msg":"version:0.0.1,error:{state: \"{}\", error: \"empty state2\"}"`) {
t.Fatalf("log does not contain error: %s", content) t.Fatalf("log does not contain error: %s", content)
} }
}) })