1 package metriton 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "io/ioutil" 8 "net/http" 9 ) 10 11 // Report is a telemetry report to submit to Metriton. 12 // 13 // See: https://github.com/datawire/metriton/blob/master/metriton/scout/jsonschema.py 14 type Report struct { 15 Application string `json:"application"` // (required) The name of the application reporting the event 16 InstallID string `json:"install_id"` // (required) Application installation ID (usually a UUID, but technically an opaque string) 17 Version string `json:"version"` // (required) Application version number 18 Metadata map[string]interface{} `json:"metadata"` // (optional) Additional metadata about the application 19 } 20 21 // Send the report to the given Metriton endpoint using the given 22 // httpClient. 23 // 24 // The returned *Response may be nil even if there is no error, if 25 // Metriton has not yet been configured to know about the Report's 26 // `.Application`; i.e. a Response is only returned for known 27 // applications. 28 func (r Report) Send(ctx context.Context, httpClient *http.Client, endpoint string) (*Response, error) { 29 body, err := json.MarshalIndent(r, "", " ") 30 if err != nil { 31 return nil, err 32 } 33 34 req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body)) 35 if err != nil { 36 return nil, err 37 } 38 req.Header.Set("Content-Type", "application/json") 39 40 resp, err := httpClient.Do(req) 41 if err != nil { 42 return nil, err 43 } 44 defer resp.Body.Close() 45 46 respBytes, err := ioutil.ReadAll(resp.Body) 47 if err != nil { 48 return nil, err 49 } 50 51 if len(respBytes) == 0 { 52 // not a recognized .Application 53 return nil, nil 54 } 55 56 var parsedResp Response 57 if err := json.Unmarshal(respBytes, &parsedResp); err != nil { 58 return nil, err 59 } 60 return &parsedResp, nil 61 } 62 63 // Response is a response from Metriton, after submitting a Report to 64 // it. 65 type Response struct { 66 AppInfo 67 68 // Only set for .Application=="aes" 69 HardLimit bool `json:"hard_limit"` 70 71 // Disable submitting any more telemetry for the remaining 72 // lifetime of this process. 73 // 74 // This way, if we ever make another release that turns out to 75 // effectively DDoS Metriton, we can adjust the Metriton 76 // server's `api.py:handle_report()` to be able to tell the 77 // offending processes to shut up. 78 DisableScout bool `json:"disable_scout"` 79 } 80 81 // AppInfo is the information that Metriton knows about an 82 // application. 83 // 84 // There isn't really an otherwise fixed schema for this; Metriton 85 // returns whatever it reads from 86 // f"s3://scout-datawire-io/{report.application}/app.json". However, 87 // looking at all of the existing app.json files, they all agree on 88 // the schema 89 type AppInfo struct { 90 Application string `json:"application"` 91 LatestVersion string `json:"latest_version"` 92 Notices []Notice `json:"notices"` 93 } 94 95 // Notice is a notice that should be displayed to the user. 96 // 97 // I have no idea what the schema for Notice is, there are none 98 // currently, and reverse-engineering it from what diagd.py consumes 99 // isn't worth the effort at this time. 100 type Notice interface{} 101