Просмотр исходного кода

Merge pull request #302 from smiclea/log-storage

Log all API calls to `localStorage`
Dorin Paslaru 7 лет назад
Родитель
Сommit
e3bf2b87aa

+ 5 - 1
src/components/molecules/UserDropdown/UserDropdown.jsx

@@ -56,7 +56,7 @@ const ListItem = styled.div`
   padding: 8px 0;
 
   &:last-child {
-    padding-bottom: 0;
+    padding: 0;
   }
 `
 
@@ -104,6 +104,7 @@ const Email = styled.div`
   border-bottom: 1px solid ${Palette.grayscale[3]};
 `
 
+
 type DictItem = { label: string, value: string }
 type Props = {
   onItemClick: (item: DictItem) => void,
@@ -187,6 +188,9 @@ class UserDropdown extends React.Component<Props, State> {
     }
 
     let items = [{
+      label: 'Download Log',
+      value: 'downloadlog',
+    }, {
       label: 'Sign Out',
       value: 'signout',
     }]

+ 10 - 1
src/components/organisms/DetailsPageHeader/DetailsPageHeader.jsx

@@ -24,6 +24,7 @@ import UserDropdown from '../../molecules/UserDropdown'
 import type { User as UserType } from '../../../types/User'
 
 import notificationStore from '../../../stores/NotificationStore'
+import logger from '../../../utils/ApiLogger'
 
 import backgroundImage from './images/star-bg.jpg'
 import logoImage from './images/logo.svg'
@@ -81,6 +82,14 @@ class DetailsPageHeader extends React.Component<Props, {}> {
     notificationStore.saveSeen()
   }
 
+  handleUserItemClick(item: { label: string, value: string }) {
+    if (item.value === 'downloadlog') {
+      logger.download()
+    } else {
+      this.props.onUserItemClick(item)
+    }
+  }
+
   pollData() {
     if (this.stopPolling) {
       return
@@ -107,7 +116,7 @@ class DetailsPageHeader extends React.Component<Props, {}> {
           <UserDropdownStyled
             white
             user={this.props.user}
-            onItemClick={this.props.onUserItemClick}
+            onItemClick={item => { this.handleUserItemClick(item) }}
             data-test-id="dpHeader-userDropdown"
           />
         </User>

+ 1 - 1
src/components/organisms/DetailsPageHeader/test.jsx

@@ -50,7 +50,7 @@ describe('DetailsPageHeader Component', () => {
   it('dispatches user item click', () => {
     let onUserItemClick = sinon.spy()
     let wrapper = wrap({ user, onUserItemClick })
-    wrapper.find('userDropdown').simulate('itemClick')
+    wrapper.find('userDropdown').simulate('itemClick', { value: '', label: '' })
     expect(onUserItemClick.called).toBe(true)
   })
 })

+ 1 - 1
src/components/organisms/Notifications/Notifications.jsx

@@ -53,7 +53,7 @@ class Notifications extends React.Component<{}> {
       message: lastNotification.title ? lastNotification.message : null,
       level: lastNotification.level || 'info',
       position: 'br',
-      autoDismiss: 10,
+      autoDismiss: lastNotification.message.length < 150 ? 10 : 30,
       action: lastNotification.options ? lastNotification.options.action : null,
     })
 

+ 4 - 0
src/components/organisms/PageHeader/PageHeader.jsx

@@ -31,6 +31,7 @@ import Endpoint from '../../organisms/Endpoint'
 import UserModal from '../../organisms/UserModal'
 import ProjectModal from '../../organisms/ProjectModal'
 
+import logger from '../../../utils/ApiLogger'
 import projectStore from '../../../stores/ProjectStore'
 import userStore from '../../../stores/UserStore'
 import notificationStore from '../../../stores/NotificationStore'
@@ -107,6 +108,9 @@ class PageHeader extends React.Component<Props, State> {
 
   handleUserItemClick(item: { value: string }) {
     switch (item.value) {
+      case 'downloadlog':
+        logger.download()
+        return
       case 'signout':
         userStore.logout()
         return

+ 40 - 4
src/utils/ApiCaller.js

@@ -18,6 +18,7 @@ import axios from 'axios'
 import type { AxiosXHRConfig, $AxiosXHR } from 'axios'
 import cookie from 'js-cookie'
 
+import logger from './ApiLogger'
 import notificationStore from '../stores/NotificationStore'
 
 type Cancelable = {
@@ -86,12 +87,22 @@ class ApiCaller {
       }
 
       if (!options.skipLog) {
-        console.log(`%cSending ${axiosOptions.method || 'GET'} Request to ${axiosOptions.url}`, 'color: #F5A623')
+        logger.log({
+          url: axiosOptions.url,
+          method: axiosOptions.method || 'GET',
+          type: 'REQUEST',
+        })
       }
 
       axios(axiosOptions).then((response) => {
         if (!options.skipLog) {
           console.log(`%cResponse ${axiosOptions.url}`, 'color: #0044CA', response.data)
+          logger.log({
+            url: axiosOptions.url,
+            method: axiosOptions.method || 'GET',
+            type: 'RESPONSE',
+            requestStatus: 200,
+          })
         }
         resolve(response)
       }).catch(error => {
@@ -115,7 +126,13 @@ class ApiCaller {
             window.location.href = '/'
           }
 
-          console.log(`%cError Response: ${axiosOptions.url}`, 'color: #D0021B', error.response)
+          logger.log({
+            url: axiosOptions.url,
+            method: axiosOptions.method || 'GET',
+            type: 'RESPONSE',
+            requestStatus: error.response.status,
+            requestError: error,
+          })
           reject(error.response)
         } else if (error.request) {
           // The request was made but no response was received
@@ -123,18 +140,37 @@ class ApiCaller {
           if (window.location.hash !== loginUrl) {
             notificationStore.alert('Request failed, there might be a problem with the connection to the server.', 'error')
           }
-          console.log(`%cError No Response: ${axiosOptions.url}`, 'color: #D0021B')
+          logger.log({
+            url: axiosOptions.url,
+            method: axiosOptions.method || 'GET',
+            type: 'RESPONSE',
+            description: 'No response',
+            requestStatus: 500,
+            requestError: error,
+          })
           reject({})
         } else {
           let canceled = error.constructor.name === 'Cancel'
           reject({ canceled })
           if (canceled) {
+            logger.log({
+              url: axiosOptions.url,
+              method: axiosOptions.method || 'GET',
+              type: 'RESPONSE',
+              requestStatus: 'canceled',
+            })
             return
           }
 
           // Something happened in setting up the request that triggered an Error
+          logger.log({
+            url: axiosOptions.url,
+            method: axiosOptions.method || 'GET',
+            type: 'RESPONSE',
+            description: 'Something happened in setting up the request',
+            requestStatus: 500,
+          })
           notificationStore.alert('Request failed, there might be a problem with the connection to the server.', 'error')
-          console.log(`%cError Something happened in setting up the request: ${axiosOptions.url}`, 'color: #D0021B')
         }
       })
     })

+ 92 - 0
src/utils/ApiLogger.js

@@ -0,0 +1,92 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+type LogType = 'REQUEST' | 'RESPONSE'
+
+type LogOptions = {
+  url: string,
+  method: string,
+  type: LogType,
+  description?: string,
+  requestStatus?: number | 'canceled',
+  requestError?: any,
+}
+
+type Log = LogOptions & {
+  date: Date,
+}
+const MAX_LOGS = 3000
+class Storage {
+  static NAME = 'apiLog'
+
+  static getLogRaw(): string {
+    return localStorage.getItem(this.NAME) || '[]'
+  }
+
+  static getLog(): Log[] {
+    let logs: Log[] = JSON.parse(localStorage.getItem(this.NAME) || '[]')
+    return logs
+  }
+
+  static saveLog(options: LogOptions) {
+    let logs: Log[] = JSON.parse(localStorage.getItem(this.NAME) || '[]')
+    let newLog: Log = {
+      date: new Date(),
+      ...options,
+    }
+
+    if (logs.length > MAX_LOGS) {
+      logs.splice(0, logs.length - MAX_LOGS)
+    }
+
+    logs.push(newLog)
+    localStorage.setItem(this.NAME, JSON.stringify(logs))
+  }
+}
+
+class ApiLogger {
+  log(options: LogOptions) {
+    if (options.type === 'REQUEST') {
+      console.log(`%cSending ${options.method} Request to ${options.url}`, 'color: #F5A623')
+    } else if (options.requestError) {
+      console.log(`%cError Response: ${options.url}`, 'color: #D0021B', options.requestError)
+    } else if (options.requestStatus === 'canceled') {
+      console.log(`%cRequest Canceled: ${options.url}`, 'color: #0044CA')
+    } else if (options.requestStatus === 500) {
+      console.log(`%cError Something happened in setting up the request: ${options.url}`, 'color: #D0021B')
+    }
+
+    if (options.requestError && options.requestError.response && options.requestError.response.data) {
+      options.requestError = options.requestError.response.data.error
+    }
+
+    Storage.saveLog(options)
+  }
+
+  download() {
+    let href: string = `data:text/json;charset=utf-8,${encodeURIComponent(Storage.getLogRaw())}`
+    let downloadAnchorNode = document.createElement('a')
+    downloadAnchorNode.setAttribute('href', href)
+    downloadAnchorNode.setAttribute('download', 'coriolis-log.json')
+    if (document.body) {
+      document.body.appendChild(downloadAnchorNode) // required for firefox
+    }
+    downloadAnchorNode.click()
+    downloadAnchorNode.remove()
+  }
+}
+
+export default new ApiLogger()