diff --git a/keyserver/src/responders/report-responders.js b/keyserver/src/responders/report-responders.js index 5614a58ad..1dce33179 100644 --- a/keyserver/src/responders/report-responders.js +++ b/keyserver/src/responders/report-responders.js @@ -1,245 +1,229 @@ // @flow import type { $Response, $Request } from 'express'; import t from 'tcomb'; import type { TInterface, TStructProps, TUnion } from 'tcomb'; +import type { BaseAction } from 'lib/types/redux-types.js'; import { type ReportCreationResponse, type ReportCreationRequest, type FetchErrorReportInfosResponse, type FetchErrorReportInfosRequest, type ThreadInconsistencyReportShape, type EntryInconsistencyReportShape, + type ActionSummary, + type ThreadInconsistencyReportCreationRequest, + type EntryInconsistencyReportCreationRequest, + type MediaMissionReportCreationRequest, + type UserInconsistencyReportCreationRequest, reportTypes, reportInfoValidator, } from 'lib/types/report-types.js'; import { userInfoValidator } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; -import { - tShape, - tPlatform, - tPlatformDetails, -} from 'lib/utils/validation-utils.js'; +import { tShape, tPlatformDetails } from 'lib/utils/validation-utils.js'; import { newEntryQueryInputValidator } from './entry-responders.js'; import createReport from '../creators/report-creator.js'; import { fetchErrorReportInfos, fetchReduxToolsImport, } from '../fetchers/report-fetchers.js'; import type { Viewer } from '../session/viewer.js'; -const tActionSummary = tShape({ +const tActionSummary = tShape({ type: t.String, time: t.Number, summary: t.String, }); +const tActionType = t.irreducible<$PropertyType>( + 'ActionType', + x => typeof x === 'string', +); const threadInconsistencyReportValidatorShape: TStructProps = { platformDetails: tPlatformDetails, beforeAction: t.Object, action: t.Object, pollResult: t.maybe(t.Object), pushResult: t.Object, - lastActionTypes: t.maybe(t.list(t.String)), + lastActionTypes: t.maybe(t.list(tActionType)), lastActions: t.maybe(t.list(tActionSummary)), time: t.maybe(t.Number), }; const entryInconsistencyReportValidatorShape: TStructProps = { platformDetails: tPlatformDetails, beforeAction: t.Object, action: t.Object, calendarQuery: newEntryQueryInputValidator, pollResult: t.maybe(t.Object), pushResult: t.Object, - lastActionTypes: t.maybe(t.list(t.String)), + lastActionTypes: t.maybe(t.list(tActionType)), lastActions: t.maybe(t.list(tActionSummary)), time: t.Number, }; const userInconsistencyReportValidatorShape = { platformDetails: tPlatformDetails, action: t.Object, beforeStateCheck: t.Object, afterStateCheck: t.Object, lastActions: t.list(tActionSummary), time: t.Number, }; -const threadInconsistencyReportCreationRequest = tShape({ - ...threadInconsistencyReportValidatorShape, - type: t.irreducible( - 'reportTypes.THREAD_INCONSISTENCY', - x => x === reportTypes.THREAD_INCONSISTENCY, - ), -}); +const threadInconsistencyReportCreationRequest = + tShape({ + ...threadInconsistencyReportValidatorShape, + type: t.irreducible( + 'reportTypes.THREAD_INCONSISTENCY', + x => x === reportTypes.THREAD_INCONSISTENCY, + ), + }); -const entryInconsistencyReportCreationRquest = tShape({ - ...entryInconsistencyReportValidatorShape, - type: t.irreducible( - 'reportTypes.ENTRY_INCONSISTENCY', - x => x === reportTypes.ENTRY_INCONSISTENCY, - ), -}); +const entryInconsistencyReportCreationRquest = + tShape({ + ...entryInconsistencyReportValidatorShape, + type: t.irreducible( + 'reportTypes.ENTRY_INCONSISTENCY', + x => x === reportTypes.ENTRY_INCONSISTENCY, + ), + }); -const mediaMissionReportCreationRequest = tShape({ - type: t.irreducible( - 'reportTypes.MEDIA_MISSION', - x => x === reportTypes.MEDIA_MISSION, - ), - platformDetails: tPlatformDetails, - time: t.Number, - mediaMission: t.Object, - uploadServerID: t.maybe(t.String), - uploadLocalID: t.maybe(t.String), - mediaLocalID: t.maybe(t.String), - messageServerID: t.maybe(t.String), - messageLocalID: t.maybe(t.String), -}); +const mediaMissionReportCreationRequest = + tShape({ + type: t.irreducible( + 'reportTypes.MEDIA_MISSION', + x => x === reportTypes.MEDIA_MISSION, + ), + platformDetails: tPlatformDetails, + time: t.Number, + mediaMission: t.Object, + uploadServerID: t.maybe(t.String), + uploadLocalID: t.maybe(t.String), + mediaLocalID: t.maybe(t.String), + messageServerID: t.maybe(t.String), + messageLocalID: t.maybe(t.String), + }); -const userInconsistencyReportCreationRequest = tShape({ - ...userInconsistencyReportValidatorShape, - type: t.irreducible( - 'reportTypes.USER_INCONSISTENCY', - x => x === reportTypes.USER_INCONSISTENCY, - ), -}); +const userInconsistencyReportCreationRequest = + tShape({ + ...userInconsistencyReportValidatorShape, + type: t.irreducible( + 'reportTypes.USER_INCONSISTENCY', + x => x === reportTypes.USER_INCONSISTENCY, + ), + }); export const reportCreationRequestInputValidator: TUnion = t.union([ tShape({ - type: t.maybe( - t.irreducible('reportTypes.ERROR', x => x === reportTypes.ERROR), + type: t.irreducible( + 'reportTypes.ERROR', + x => x === reportTypes.ERROR, ), - platformDetails: t.maybe(tPlatformDetails), - deviceType: t.maybe(tPlatform), - codeVersion: t.maybe(t.Number), - stateVersion: t.maybe(t.Number), + platformDetails: tPlatformDetails, errors: t.list( tShape({ errorMessage: t.String, stack: t.maybe(t.String), componentStack: t.maybe(t.String), }), ), preloadedState: t.Object, currentState: t.Object, - actions: t.list(t.union([t.Object, t.String])), + actions: t.list(t.Object), }), threadInconsistencyReportCreationRequest, entryInconsistencyReportCreationRquest, mediaMissionReportCreationRequest, userInconsistencyReportCreationRequest, ]); export const reportCreationResponseValidator: TInterface = tShape({ id: t.String }); async function reportCreationResponder( viewer: Viewer, request: ReportCreationRequest, ): Promise { if (request.type === null || request.type === undefined) { request.type = reportTypes.ERROR; } if (!request.platformDetails && request.deviceType) { const { deviceType, codeVersion, stateVersion, ...rest } = request; request = { ...rest, platformDetails: { platform: deviceType, codeVersion, stateVersion }, }; } const response = await createReport(viewer, request); if (!response) { throw new ServerError('ignored_report'); } return response; } export const reportMultiCreationRequestInputValidator: TInterface = tShape({ - reports: t.list( - t.union([ - tShape({ - type: t.irreducible( - 'reportTypes.ERROR', - x => x === reportTypes.ERROR, - ), - platformDetails: tPlatformDetails, - errors: t.list( - tShape({ - errorMessage: t.String, - stack: t.maybe(t.String), - componentStack: t.maybe(t.String), - }), - ), - preloadedState: t.Object, - currentState: t.Object, - actions: t.list(t.union([t.Object, t.String])), - }), - threadInconsistencyReportCreationRequest, - entryInconsistencyReportCreationRquest, - mediaMissionReportCreationRequest, - userInconsistencyReportCreationRequest, - ]), - ), + reports: t.list(reportCreationRequestInputValidator), }); type ReportMultiCreationRequest = { - reports: $ReadOnlyArray, + +reports: $ReadOnlyArray, }; async function reportMultiCreationResponder( viewer: Viewer, request: ReportMultiCreationRequest, ): Promise { await Promise.all( request.reports.map(reportCreationRequest => createReport(viewer, reportCreationRequest), ), ); } export const fetchErrorReportInfosRequestInputValidator: TInterface = tShape({ cursor: t.maybe(t.String), }); export const fetchErrorReportInfosResponseValidator: TInterface = tShape({ reports: t.list(reportInfoValidator), userInfos: t.list(userInfoValidator), }); async function errorReportFetchInfosResponder( viewer: Viewer, request: FetchErrorReportInfosRequest, ): Promise { return await fetchErrorReportInfos(viewer, request); } async function errorReportDownloadResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const id = req.params.reportID; if (!id) { throw new ServerError('invalid_parameters'); } const result = await fetchReduxToolsImport(viewer, id); res.set('Content-Disposition', `attachment; filename=report-${id}.json`); res.json({ preloadedState: JSON.stringify(result.preloadedState), payload: JSON.stringify(result.payload), }); } export { threadInconsistencyReportValidatorShape, entryInconsistencyReportValidatorShape, reportCreationResponder, reportMultiCreationResponder, errorReportFetchInfosResponder, errorReportDownloadResponder, }; diff --git a/lib/types/device-types.js b/lib/types/device-types.js index 1c2cff71b..2db70946e 100644 --- a/lib/types/device-types.js +++ b/lib/types/device-types.js @@ -1,57 +1,57 @@ // @flow import invariant from 'invariant'; import { values } from '../utils/objects.js'; const deviceTypesObj = Object.freeze({ ios: 'ios', android: 'android', }); export type DeviceType = $Values; export type Platform = DeviceType | 'web' | 'windows' | 'macos'; export const deviceTypes: $ReadOnlyArray = values(deviceTypesObj); export function isDeviceType(platform: ?string): boolean { return platform === 'ios' || platform === 'android'; } export function assertDeviceType(deviceType: ?string): DeviceType { invariant( deviceType === 'ios' || deviceType === 'android', 'string is not DeviceType enum', ); return deviceType; } export function isWebPlatform(platform: ?string): boolean { return platform === 'web' || platform === 'windows' || platform === 'macos'; } export type DeviceTokenUpdateRequest = { +deviceToken: ?string, +deviceType?: DeviceType, +platformDetails?: PlatformDetails, }; export type PlatformDetails = { +platform: Platform, - +codeVersion?: number, - +stateVersion?: number, + +codeVersion?: ?number, + +stateVersion?: ?number, }; export type LastCommunicatedPlatformDetails = { +platformDetails: PlatformDetails, +keyserverID: string, }; export type VersionResponse = { +codeVersion: number, +ownerUsername: ?string, +ownerID: ?string, }; export type GetVersionActionPayload = { +versionResponses: { +[keyserverId: string]: VersionResponse }, }; diff --git a/lib/types/report-types.js b/lib/types/report-types.js index 9f81536ec..ceabebce6 100644 --- a/lib/types/report-types.js +++ b/lib/types/report-types.js @@ -1,235 +1,235 @@ // @flow import invariant from 'invariant'; import t, { type TInterface } from 'tcomb'; import { type PlatformDetails } from './device-types.js'; import { type RawEntryInfo, type CalendarQuery } from './entry-types.js'; import { type MediaMission } from './media-types.js'; import type { AppState, BaseAction } from './redux-types.js'; import { type RawThreadInfos } from './thread-types.js'; import type { UserInfo, UserInfos } from './user-types.js'; import { tPlatformDetails, tShape } from '../utils/validation-utils.js'; export type EnabledReports = { +crashReports: boolean, +inconsistencyReports: boolean, +mediaReports: boolean, }; export type SupportedReports = $Keys; export const defaultEnabledReports: EnabledReports = { crashReports: false, inconsistencyReports: false, mediaReports: false, }; export const defaultDevEnabledReports: EnabledReports = { crashReports: true, inconsistencyReports: true, mediaReports: true, }; export type ReportStore = { +enabledReports: EnabledReports, +queuedReports: $ReadOnlyArray, }; export const reportTypes = Object.freeze({ ERROR: 0, THREAD_INCONSISTENCY: 1, ENTRY_INCONSISTENCY: 2, MEDIA_MISSION: 3, USER_INCONSISTENCY: 4, }); type ReportType = $Values; export function assertReportType(reportType: number): ReportType { invariant( reportType === 0 || reportType === 1 || reportType === 2 || reportType === 3 || reportType === 4, 'number is not ReportType enum', ); return reportType; } export type ErrorInfo = { componentStack: string, ... }; export type ErrorData = { error: Error, info?: ErrorInfo }; export type FlatErrorData = { errorMessage: string, stack?: string, componentStack?: ?string, }; export type ActionSummary = { +type: $PropertyType, +time: number, +summary: string, }; export type ThreadInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: RawThreadInfos, +action: BaseAction, - +pollResult?: RawThreadInfos, + +pollResult?: ?RawThreadInfos, +pushResult: RawThreadInfos, - +lastActionTypes?: $ReadOnlyArray<$PropertyType>, - +lastActions?: $ReadOnlyArray, - +time?: number, + +lastActionTypes?: ?$ReadOnlyArray<$PropertyType>, + +lastActions?: ?$ReadOnlyArray, + +time?: ?number, }; export type EntryInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: { +[id: string]: RawEntryInfo }, +action: BaseAction, +calendarQuery: CalendarQuery, - +pollResult?: { +[id: string]: RawEntryInfo }, + +pollResult?: ?{ +[id: string]: RawEntryInfo }, +pushResult: { +[id: string]: RawEntryInfo }, - +lastActionTypes?: $ReadOnlyArray<$PropertyType>, - +lastActions?: $ReadOnlyArray, + +lastActionTypes?: ?$ReadOnlyArray<$PropertyType>, + +lastActions?: ?$ReadOnlyArray, +time: number, }; export type UserInconsistencyReportShape = { +platformDetails: PlatformDetails, +action: BaseAction, +beforeStateCheck: UserInfos, +afterStateCheck: UserInfos, +lastActions: $ReadOnlyArray, +time: number, }; export type ErrorReportCreationRequest = { +type: 0, +platformDetails: PlatformDetails, +errors: $ReadOnlyArray, +preloadedState: AppState, +currentState: AppState, +actions: $ReadOnlyArray, }; export type ThreadInconsistencyReportCreationRequest = { ...ThreadInconsistencyReportShape, +type: 1, }; export type EntryInconsistencyReportCreationRequest = { ...EntryInconsistencyReportShape, +type: 2, }; export type MediaMissionReportCreationRequest = { +type: 3, +platformDetails: PlatformDetails, +time: number, // ms +mediaMission: MediaMission, +uploadServerID?: ?string, +uploadLocalID?: ?string, +mediaLocalID?: ?string, // deprecated +messageServerID?: ?string, +messageLocalID?: ?string, }; export type UserInconsistencyReportCreationRequest = { ...UserInconsistencyReportShape, +type: 4, }; export type ReportCreationRequest = | ErrorReportCreationRequest | ThreadInconsistencyReportCreationRequest | EntryInconsistencyReportCreationRequest | MediaMissionReportCreationRequest | UserInconsistencyReportCreationRequest; export type ClientThreadInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: RawThreadInfos, +action: BaseAction, +pushResult: RawThreadInfos, +lastActions: $ReadOnlyArray, +time: number, }; export type ClientEntryInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: { +[id: string]: RawEntryInfo }, +action: BaseAction, +calendarQuery: CalendarQuery, +pushResult: { +[id: string]: RawEntryInfo }, +lastActions: $ReadOnlyArray, +time: number, }; export type ClientErrorReportCreationRequest = { ...ErrorReportCreationRequest, +id: string, }; export type ClientThreadInconsistencyReportCreationRequest = { ...ClientThreadInconsistencyReportShape, +type: 1, +id: string, }; export type ClientEntryInconsistencyReportCreationRequest = { ...ClientEntryInconsistencyReportShape, +type: 2, +id: string, }; export type ClientMediaMissionReportCreationRequest = { ...MediaMissionReportCreationRequest, +id: string, }; export type ClientUserInconsistencyReportCreationRequest = { ...UserInconsistencyReportCreationRequest, +id: string, }; export type ClientReportCreationRequest = | ClientErrorReportCreationRequest | ClientThreadInconsistencyReportCreationRequest | ClientEntryInconsistencyReportCreationRequest | ClientMediaMissionReportCreationRequest | ClientUserInconsistencyReportCreationRequest; export type QueueReportsPayload = { +reports: $ReadOnlyArray, }; export type ClearDeliveredReportsPayload = { +reports: $ReadOnlyArray, }; export type ReportCreationResponse = { +id: string, }; // Reports Service specific types export type ReportsServiceSendReportsRequest = | ClientReportCreationRequest | $ReadOnlyArray; export type ReportsServiceSendReportsResponse = { +reportIDs: $ReadOnlyArray, }; // Keyserver specific types type ReportInfo = { +id: string, +viewerID: string, +platformDetails: PlatformDetails, +creationTime: number, }; export const reportInfoValidator: TInterface = tShape({ id: t.String, viewerID: t.String, platformDetails: tPlatformDetails, creationTime: t.Number, }); export type FetchErrorReportInfosRequest = { +cursor: ?string, }; export type FetchErrorReportInfosResponse = { +reports: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; export type ReduxToolsImport = { +preloadedState: AppState, +payload: $ReadOnlyArray, };