feat(error_reporting): integrate error reporting
This commit is contained in:
parent
9a7cfcb097
commit
711a3a874f
9 changed files with 86 additions and 57 deletions
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue