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()
}
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")).
AddCookie(token).
Send(report).
Send(reports).
End()
}

View file

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

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 {
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({
method: "post",
url: `${this.url}/v1/settings/errors`,
data: {
content,
version,
reports: reports.toArray(),
},
});
};

View file

@ -7,7 +7,7 @@ export interface SettingsClientResps {
healthMockResp?: Response;
setClientCfgMockResp?: Response;
getClientCfgMockResp?: Response<ClientConfigMsg>;
reportErrorResp?: Response;
reportErrorsResp?: Response;
}
export const resps = {
@ -29,7 +29,7 @@ export const resps = {
},
},
},
reportErrorResp: {
reportErrorsResp: {
status: 200,
statusText: "",
data: {},
@ -66,7 +66,7 @@ export class MockSettingsClient {
return this.wrapPromise(this.resps.getClientCfgMockResp);
};
reportError = (): Promise<Response> => {
return this.wrapPromise(this.resps.reportErrorResp);
reportErrors = (): Promise<Response> => {
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 { ILocalStorage, Storage } from "./localstorage";
@ -6,13 +6,12 @@ import { ISettingsClient } from "../client";
import { SettingsClient } from "../client/settings";
import { ICoreState } from "../components/core_state";
import { updater } from "../components/state_updater";
import { alertMsg } from "./env";
import { ClientErrorReport } from "../client";
const errorVer = "0.0.1";
const cookieKeyClErrs = "qs_cli_errs";
export interface ClientErrorV001 {
version: string;
error: string;
timestamp: string;
state: ICoreState;
@ -72,7 +71,6 @@ export class SimpleErrorLogger {
try {
const sign = this.getErrorSign(msg);
const clientErr: ClientErrorV001 = {
version: errorVer,
error: msg,
timestamp: `${Date.now()}`,
state: updater().props,
@ -92,16 +90,21 @@ export class SimpleErrorLogger {
report = async (): Promise<null | Error> => {
try {
const errs = this.readErrs();
let reports = List<ClientErrorReport>();
for (let sign of errs.keySeq().toArray()) {
const err = errs.get(sign);
const resp = await this.client.reportError(sign, JSON.stringify(err));
const errObj = errs.get(sign);
reports = reports.push({
report: JSON.stringify(errObj),
version: errorVer,
});
}
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) {
return Error(e);
}

View file

@ -179,6 +179,32 @@ export class PaneSettings extends React.Component<Props, State, {}> {
render() {
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 (
<div id="pane-settings">
@ -415,29 +441,7 @@ export class PaneSettings extends React.Component<Props, State, {}> {
</div>
</Container>
<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>
{errorReportPane}
{/* <div className="hr"></div>
<div>

View file

@ -83,14 +83,20 @@ type ClientErrorReport struct {
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
req := &ClientErrorReport{}
req := &ClientErrorReports{}
if err = c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 400, err))
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))
}

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.GET("/client", settingsSvc.GetClientCfg)
settingsAPI.PATCH("/client", settingsSvc.SetClientCfg)
settingsAPI.POST("/errors", settingsSvc.ReportError)
settingsAPI.POST("/errors", settingsSvc.ReportErrors)
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)
reportContent := `{state: "{}", error: "empty state"}`
report := &settings.ClientErrorReport{
Report: reportContent,
reports := &settings.ClientErrorReports{
Reports: []*settings.ClientErrorReport{
&settings.ClientErrorReport{
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 {
t.Fatal(errs)
} else if reportResp.StatusCode != 200 {
@ -154,7 +161,10 @@ func TestSettingsHandlers(t *testing.T) {
if err != nil {
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)
}
})