├── .gitignore ├── LICENSE ├── README.md ├── __tests__ └── client │ ├── aqlQueryParser.js │ ├── useAqlMutation.js │ └── useAqlSubscription.js ├── package.json ├── src ├── client │ ├── aqlQueryParser.js │ ├── useAqlMutation.js │ └── useAqlSubscription.js └── index.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | package-lock.json 44 | webpack.local.config.js 45 | webpack.production.config.js 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aqls-Client 2 | 3 | 4 | ## Overview 5 | GraphQL analytics toolkit and dashboard that integrate with Apollo Client and Apollo Server Express. It is an easy to use analytics suite that monitors GraphQL subscriptions concurrency, latency, errors, and resolver frequency. Our integrated dashboard displays your GraphQL analytics on dynamic and interactive charts. 6 | 7 | This package is for setting up your client-side. For the server-side package please refer to the [Aqls-server](https://github.com/oslabs-beta/Aqls-server) package. 8 | 9 | **Note:** Aqls is currently in BETA and improvements will continue to be implemented. If any issues are encountered while using our application, please submit a PR. 10 | 11 | 12 | ## Requirements 13 | - **React** *version* 16.8+ 14 | - **apollo-client** *version* 3.2.1+ 15 | 16 | ## Install 17 | With npm: 18 | 19 | ``` 20 | npm install --save @aqls/client 21 | ``` 22 | 23 | ## Tracking GraphQL Subscriptions 24 | 25 | #### 26 | - [ ] **1**. Import useqAqlSubscription: 27 | 28 | ```javascript 29 | import { useAqlSubscription } from '@aqls/client'; 30 | ``` 31 | #### 32 | - [ ] **2**. Include aql in GraphQL query: 33 | ```javascript 34 | const colorSubscription = gql` 35 | subscription { 36 | updatedColor { 37 | cssColor 38 | aql { 39 | mutationSendTime 40 | mutationReceived 41 | subscriberReceived 42 | mutationId 43 | resolver 44 | userToken 45 | } 46 | } 47 | } 48 | `; 49 | ``` 50 | #### 51 | - [ ] **3**. Invoke useAqlSubscription hook: 52 | ```javascript 53 | const { data, loading, error } = useAqlSubscription( 54 | graphQLQuery, 55 | { optionsObject }, 56 | subscriptionResolver, 57 | ); 58 | ``` 59 | **Note:** optionsObject accepts any options available in the [Apollo-client useSubscription](https://www.apollographql.com/docs/react/data/subscriptions/#options) hook 60 | ### 61 | **useAqlSubscription hook example:** 62 | ```javascript 63 | const { data, loading, error } = useAqlSubscription( 64 | colorSubscription, 65 | { 66 | onSubscriptionData: (client) => { 67 | setColor(client.subscriptionData.data.updatedColor.cssColor); 68 | }, 69 | }, 70 | 'updatedColor', 71 | ); 72 | ``` 73 | #### 74 | ## Tracking GraphQL Mutations 75 | 76 | #### 77 | - [ ] **1**. Import useqAqlMutation: 78 | 79 | ```javascript 80 | import { useAqlMutation } from '@aqls/client'; 81 | ``` 82 | #### 83 | - [ ] **2**. Invoke useAqlMutation hook: 84 | ```javascript 85 | useAqlMutation(query) 86 | ``` 87 | This hook takes a GraphQL query and automatically injects analytics into it. You can use `.then()` if you want to access async functionality. Below is an example using the hook as part of a click handler. 88 | 89 | ```javascript 90 | const handleClick = (chosenColor) => { 91 | const colorQuery = `mutation{newColor(colorArg: "${chosenColor}"){id cssColor}}`; 92 | useAqlMutation(colorQuery) 93 | .then((data) => 94 | setColor(data.chosenColor) 95 | ) 96 | .catch((err) => setError(err)); 97 | ``` 98 | #### 99 | #### 100 | 101 | - [ ] **Lastly, Connect with the Aqls Team!** 102 | 103 | Visit our website: [Aqls.io](https://www.aqls.io/) 104 | 105 | Contact us: aqlorgteam@gmail.com 106 | 107 | Case Simmons: [Case's Github](https://github.com/casesimmons) and [Case's LinkedIn](https://www.linkedin.com/in/case-simmons/) 108 | 109 | Julie Pinchak: [Julie's Github](https://github.com/jpinchak) and [Julie's LinkedIn](https://www.linkedin.com/in/julie-pinchak/) 110 | 111 | Michael O'Halloran: [Michael's Github](https://github.com/LordRegis22) and [Michael's LinkedIn](https://www.linkedin.com/) 112 | 113 | Rocio Infante: [Rocio's Github](https://github.com/Rocio-Infante) and [Rocio's LinkedIn](https://www.linkedin.com/in/rocio-infante/) 114 | -------------------------------------------------------------------------------- /__tests__/client/aqlQueryParser.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /__tests__/client/useAqlMutation.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Aqls-client/7c123f0da34f4ae96b2059beb79572217623654e/__tests__/client/useAqlMutation.js -------------------------------------------------------------------------------- /__tests__/client/useAqlSubscription.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Aqls-client/7c123f0da34f4ae96b2059beb79572217623654e/__tests__/client/useAqlSubscription.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aqls/client", 3 | "version": "1.0.5", 4 | "description": "An intelligent full-stack GraphQL subscription and analytics module. Client-side hooks, automatic query parsing, and analytic generation.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "test", 8 | "build": "webpack --mode production", 9 | "prepublish": "rm -rf ./dist && npm run build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/oslabs-beta/Aqls-client" 14 | }, 15 | "keywords": [ 16 | "GraphQL", 17 | "Apollo", 18 | "Apollo-Server", 19 | "Apollo-Client", 20 | "subscriptions", 21 | "analytics" 22 | ], 23 | "author": "AqlOrg", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/oslabs-beta/Aqls-client/issues" 27 | }, 28 | "homepage": "https://aqls.io", 29 | "dependencies": { 30 | "@apollo/client": "^3.2.5", 31 | "@babel/core": "^7.12.3", 32 | "babel-loader": "^8.1.0", 33 | "graphql": "^15.3.0", 34 | "timesync": "^1.0.8", 35 | "uuid": "^8.3.1", 36 | "webpack": "^5.1.3", 37 | "webpack-cli": "^4.1.0" 38 | }, 39 | "peerDependencies": { 40 | "react": "^16.13.0", 41 | "react-dom": "^15.0.1" 42 | }, 43 | "devDependencies": { 44 | "react": "^16.13.0", 45 | "react-dom": "^15.0.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/aqlQueryParser.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import * as timesync from 'timesync'; 3 | 4 | /* aqlQueryParser uses classic iterative parsing to extract the resolver from the query, 5 | inject a correctly formatted AQL containing the resolver into the body of the query arguments.*/ 6 | 7 | // create timeSync object 8 | const ts = timesync.create({ 9 | server: '/aqlsanalytics/timesync', 10 | interval: 10000, 11 | }); 12 | 13 | function aqlQueryParser(queryString) { 14 | let returnQuery = ''; 15 | let inResolver = false; 16 | let resolver = ''; 17 | let inArgs = false; 18 | let resolverFound = false; 19 | for (let i = 0; i < queryString.length; i++) { 20 | if (inResolver && (queryString[i] === '{' || queryString[i] === '(')) { 21 | resolverFound = true; 22 | inResolver = false; 23 | } 24 | if (inResolver) { 25 | resolver += queryString[i]; 26 | } 27 | if (queryString[i] === '(') { 28 | inArgs = true; 29 | } 30 | if (queryString[i] === ')' && inArgs) { 31 | //inject aql 32 | returnQuery += `, aql: {mutationSendTime: "${ts.now()}", 33 | mutationReceived: "", 34 | subscriberReceived: "", 35 | mutationId: "${uuidv4()}", 36 | resolver: "${resolver}"}`; 37 | } 38 | if (queryString[i] === '{' && !resolverFound) { 39 | inResolver = true; 40 | } 41 | returnQuery += queryString[i]; 42 | } 43 | return returnQuery; 44 | } 45 | 46 | export default aqlQueryParser; 47 | -------------------------------------------------------------------------------- /src/client/useAqlMutation.js: -------------------------------------------------------------------------------- 1 | import aqlQueryParser from './aqlQueryParser'; 2 | 3 | /* useAqlMutation is a promisified query hook that takes a GraphQL mutation query formatted 4 | as a string, and returns the response from the server. AQL tracking is automatically injected 5 | into the body of the request. You can chain methods using .then to control app behavior on 6 | resolution of the query. */ 7 | 8 | function useAqlMutation(query) { 9 | return new Promise((resolve, reject) => { 10 | const options = { 11 | method: 'post', 12 | headers: { 'Content-Type': 'application/json' }, 13 | body: JSON.stringify({ 14 | query: aqlQueryParser(query), 15 | }), 16 | }; 17 | fetch(`/graphql`, options) 18 | .then((data) => data.json()) 19 | .then((result) => resolve(result)) 20 | .catch((err) => console.log(err)); 21 | }); 22 | } 23 | 24 | export default useAqlMutation; 25 | -------------------------------------------------------------------------------- /src/client/useAqlSubscription.js: -------------------------------------------------------------------------------- 1 | import { useSubscription } from '@apollo/client'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import * as timesync from 'timesync'; 4 | 5 | function useAqlSubscription(query, options, subscriptionResolver) { 6 | //synchronize time with timesync server 7 | const ts = timesync.create({ 8 | server: '/aqlsanalytics/timesync', 9 | interval: 10000, 10 | }); 11 | 12 | function sendAqlToAnalytics(client, subscriptionResolver) { 13 | // create final properties on Aql 14 | const aqlToSendToDB = 15 | client.subscriptionData.data[subscriptionResolver].aql; 16 | aqlToSendToDB.subscriberReceived = ts.now(); 17 | aqlToSendToDB.roundtripTime = `${ 18 | aqlToSendToDB.subscriberReceived - aqlToSendToDB.mutationSendTime 19 | }`; 20 | 21 | // extract variable names for post request 22 | const { 23 | mutationSendTime, 24 | mutationReceived, 25 | subscriberReceived, 26 | roundtripTime, 27 | mutationId, 28 | resolver, 29 | userToken, 30 | } = aqlToSendToDB; 31 | 32 | // create options object for post request that will send Aql to analytics endpoint 33 | const options = { 34 | method: 'post', 35 | headers: { 'Content-Type': 'application/json' }, 36 | body: JSON.stringify({ 37 | id: uuidv4(), 38 | mutationSendTime, 39 | mutationReceived, 40 | subscriberReceived, 41 | roundtripTime, 42 | mutationId, 43 | resolver, 44 | userToken, 45 | }), 46 | }; 47 | 48 | // send Aql to /analytics 49 | fetch(`/aqlsanalytics`, options).catch((err) => console.log(err)); 50 | } 51 | // make copy of options object 52 | const newOptions = { ...options }; 53 | // if functions are declared in the onSubscriptionData property 54 | if (newOptions.onSubscriptionData) { 55 | // save the functions and call sendAqlToAnalytics first 56 | const holder = newOptions.onSubscriptionData; 57 | newOptions.onSubscriptionData = (client) => { 58 | sendAqlToAnalytics(client, subscriptionResolver); 59 | //holder function to be called 60 | holder(client); 61 | }; 62 | } else { 63 | // otherwise call sendAqlToAnalytics 64 | newOptions.onSubscriptionData = (client) => { 65 | sendAqlToAnalytics(client, subscriptionResolver); 66 | }; 67 | } 68 | // call useSubscription, passing newOptions object 69 | const { data, loading, error } = useSubscription(query, newOptions); 70 | 71 | // return data, loading, error object 72 | return { 73 | data, 74 | loading, 75 | error, 76 | }; 77 | } 78 | 79 | export default useAqlSubscription; 80 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // import client modules 2 | //import aqlQueryParser from './client/aqlQueryParser'; 3 | import useAqlMutation from './client/useAqlMutation'; 4 | import useAqlSubscription from './client/useAqlSubscription'; 5 | 6 | //export all modules 7 | export { useAqlSubscription, useAqlMutation }; 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './src/index.js', 6 | output: { 7 | path: path.resolve('dist'), 8 | filename: 'index.js', 9 | libraryTarget: 'commonjs2', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js?$/, 15 | exclude: /(node_modules)/, 16 | use: 'babel-loader', 17 | }, 18 | ], 19 | }, 20 | externals: { 21 | // Don't bundle react or react-dom 22 | react: { 23 | commonjs: 'react', 24 | commonjs2: 'react', 25 | amd: 'React', 26 | root: 'React', 27 | }, 28 | 'react-dom': { 29 | commonjs: 'react-dom', 30 | commonjs2: 'react-dom', 31 | amd: 'ReactDOM', 32 | root: 'ReactDOM', 33 | }, 34 | }, 35 | resolve: { 36 | extensions: ['.js'], 37 | fallback: { 38 | react: path.resolve(__dirname, './node_modules/react'), 39 | 'react-dom': path.resolve(__dirname, './node_modules/react-dom'), 40 | }, 41 | }, 42 | }; 43 | --------------------------------------------------------------------------------