diff --git a/node/events-to-ga/.gitignore b/node/events-to-ga/.gitignore new file mode 100644 index 00000000..e93d8b55 --- /dev/null +++ b/node/events-to-ga/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/node/events-to-ga/.prettierrc.json b/node/events-to-ga/.prettierrc.json new file mode 100644 index 00000000..0a725205 --- /dev/null +++ b/node/events-to-ga/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/node/events-to-ga/README.md b/node/events-to-ga/README.md new file mode 100644 index 00000000..08426b20 --- /dev/null +++ b/node/events-to-ga/README.md @@ -0,0 +1,86 @@ +# ⚡ Google Analytics Event Reporter + +The function will be triggered by configured Appwrite events and report these events to Google Analytics. + +## 🧰 Usage + +### POST / + +Gets the event from the header. + +**Parameters** + +| Name | Description | Location | Type | Sample Value | +| ------------------ | ------------------------------ | -------- | ------ | --------------------- | +| x-appwrite-user-id | User ID from Appwrite. | Header | String | 642...7cd | +| x-appwrite-event | Describes the triggering event | Header | String | users.65...f19.delete | +| x-appwrite-trigger | Type of trigger for function | Header | String | event | + +**Response** + +Sample `200` Response: + +```json +{ + "ok": true, + "message": "event users.653...df19.delete is send to google analytics" +} +``` + +Sample `503` Response: +503 response is send when the response status code while sending event to Google Analytics is not within 200-299 range + +```json +{ + "ok": false, + "message": "Response status code when posting event to Google Analytics is 503" +} +``` + +Sample `401` Response: + +```json +{ "ok": false, "message": "Error Posting Event to Google Analytics" } +``` + +## ⚙️ Configuration + +| Setting | Value | +| -------------- | ------------- | +| Runtime | Node (18.0) | +| Entrypoint | `src/main.js` | +| Build Commands | `npm install` | +| Permissions | | +| Events | all | + +## 🔒 Environment Variables + +### APPWRITE_FUNCTION_PROJECT_ID + +Project ID in which Appwrite Function is added. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `6524d.....6e5` | +| Documentation | [Appwrite: Getting Started for Server](https://appwrite.io/docs/getting-started-for-server#apiKey) | + +### GA4_MEASUREMENT_ID + +API Key to talk to Appwrite backend APIs. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `G-NY5...26R` | +| Documentation | [Google Analytics 4 docs](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference) | + +### GA4_API_SECRET + +API Key to talk to Appwrite backend APIs. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------------------------------------------- | +| Required | Yes / No | +| Sample Value | `d1efb...aec35` | +| Documentation | [Google Analytics 4 docs](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference) | diff --git a/node/events-to-ga/env.d.ts b/node/events-to-ga/env.d.ts new file mode 100644 index 00000000..632cfdc1 --- /dev/null +++ b/node/events-to-ga/env.d.ts @@ -0,0 +1,10 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + GA4_MEASUREMENT_ID: string; + GA4_API_SECRET: string; + } + } +} + +export {}; diff --git a/node/events-to-ga/package-lock.json b/node/events-to-ga/package-lock.json new file mode 100644 index 00000000..3ecee48e --- /dev/null +++ b/node/events-to-ga/package-lock.json @@ -0,0 +1,271 @@ +{ + "name": "events-to-ga", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "events-to-ga", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "node-appwrite": "^11.0.0", + "undici": "^5.27.0" + }, + "devDependencies": { + "prettier": "3.0.3" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", + "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-appwrite": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-11.0.0.tgz", + "integrity": "sha512-l+O0d1kCvY56NdlmsiWw91IlUzJszNW7FZodGCyvNlJeR4+JIpy9Mica3T4M+zvZGsbRqLhpghCysQmPBwwhQA==", + "dependencies": { + "axios": "^1.4.0", + "form-data": "^4.0.0" + } + }, + "node_modules/node-appwrite/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/undici": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.0.tgz", + "integrity": "sha512-l3ydWhlhOJzMVOYkymLykcRRXqbUaQriERtR70B9LzNkZ4bX52Fc8wbTDneMiwo8T+AemZXvXaTx+9o5ROxrXg==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + } + }, + "dependencies": { + "@fastify/busboy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", + "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "node-appwrite": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-11.0.0.tgz", + "integrity": "sha512-l+O0d1kCvY56NdlmsiWw91IlUzJszNW7FZodGCyvNlJeR4+JIpy9Mica3T4M+zvZGsbRqLhpghCysQmPBwwhQA==", + "requires": { + "axios": "^1.4.0", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "undici": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.0.tgz", + "integrity": "sha512-l3ydWhlhOJzMVOYkymLykcRRXqbUaQriERtR70B9LzNkZ4bX52Fc8wbTDneMiwo8T+AemZXvXaTx+9o5ROxrXg==", + "requires": { + "@fastify/busboy": "^2.0.0" + } + } + } +} diff --git a/node/events-to-ga/package.json b/node/events-to-ga/package.json new file mode 100644 index 00000000..59966ae6 --- /dev/null +++ b/node/events-to-ga/package.json @@ -0,0 +1,19 @@ +{ + "name": "events-to-ga", + "version": "1.0.0", + "description": "", + "main": "src/main.js", + "type": "module", + "scripts": { + "format": "prettier --write ." + }, + "author": "", + "license": "ISC", + "dependencies": { + "node-appwrite": "^11.0.0", + "undici": "^5.27.0" + }, + "devDependencies": { + "prettier": "3.0.3" + } +} diff --git a/node/events-to-ga/src/main.js b/node/events-to-ga/src/main.js new file mode 100644 index 00000000..98fa38ef --- /dev/null +++ b/node/events-to-ga/src/main.js @@ -0,0 +1,79 @@ +import { fetch } from 'undici'; +import { + verifyHeaders, + formatIntoGoogleAnalyticsEvent, + throwIfMissing, +} from './utils.js'; + +export default async ({ res, req, log, error }) => { + throwIfMissing(process.env, ['GA4_MEASUREMENT_ID', 'GA4_API_SECRET']); + try { + verifyHeaders(req); + } catch (err) { + error(err); + return res.json({ ok: false, error: 'Invalid Event Header' }, 401); + } + try { + const { event_name, wildCardObject } = formatIntoGoogleAnalyticsEvent( + `${req.headers['x-appwrite-event']}` + ); + // Documentation on Google Analytics Payload https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload + const payload = JSON.stringify({ + client_id: `${req.headers['x-appwrite-user-id']}`, + user_id: `${req.headers['x-appwrite-user-id']}`, + events: [ + { + name: event_name, + params: wildCardObject, + }, + ], + }); + log(payload); + const response = await fetch( + `https://www.google-analytics.com/mp/collect?measurement_id=${process.env.GA4_MEASUREMENT_ID}&api_secret=${process.env.GA4_API_SECRET}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + cors: 'no-cors', + body: payload, + } + ); + if (response.ok) { + if (response.status != 204) { + const responseJson = await response.json(); + log(JSON.stringify(responseJson)); + } + log( + `event ${req.headers['x-appwrite-event']} is send to google analytics` + ); + } else { + error( + 'Response status code is not between 200-299 event to Google Analytics' + ); + return res.json( + { + ok: false, + error: `Response status code when posting event to Google Analytics is ${response.status}`, + }, + 503 + ); + } + } catch (err) { + error('Error reporting event to Google Analytics :', err); + return res.json( + { ok: false, error: 'Error Posting Event to Google Analytics' }, + 401 + ); + } + + log('Event posted to Google Analytics successfully'); + return res.json( + { + ok: true, + message: `event ${req.headers['x-appwrite-event']} is send to google analytics`, + }, + 200 + ); +}; diff --git a/node/events-to-ga/src/utils.js b/node/events-to-ga/src/utils.js new file mode 100644 index 00000000..a554eb3d --- /dev/null +++ b/node/events-to-ga/src/utils.js @@ -0,0 +1,67 @@ +/** + * Throws an error if any of the keys are missing from the object + * @param {*} obj + * @param {string[]} keys + * @throws {Error} + */ +export function throwIfMissing(obj, keys) { + const missing = []; + for (let key of keys) { + if (!(key in obj) || !obj[key]) { + missing.push(key); + } + } + if (missing.length > 0) { + throw new Error(`Missing required fields: ${missing.join(', ')}`); + } +} + +/** + * Returns whether the request headers and appropriate message are ok + * @param {*} req + * @returns {} + */ +export function verifyHeaders(req) { + if (req.headers['x-appwrite-user-id'] == '') { + throw new Error(`x-appwrite-user-id value in req.headers is not there`); + } + if (req.headers['x-appwrite-trigger'] != 'event') { + throw new Error( + `Not triggered by event but by ${req.headers['x-appwrite-trigger']}` + ); + } + if (req.headers['x-appwrite-event'] == '') { + throw new Error(`x-appwrite-event value in req.headers is not there`); + } +} + +/** + * Returns object containing a string denoting the formatted event name to Google Analytics and another object containing params + * @param {string} str + * @returns {{event_name:string,wildCardObject:{[key:string]:string}}} + */ +export function formatIntoGoogleAnalyticsEvent(str) { + const oddElemArray = []; + const wildCardArray = []; + const splitArray = str.split('.'); + + for (let i = 0; i < splitArray.length; i++) { + if (i % 2 == 0) oddElemArray.push(splitArray[i]); + else { + let wildCardKey = oddElemArray[oddElemArray.length - 1]; + wildCardArray.push([ + `${ + wildCardKey.charAt(wildCardKey.length - 1) === 's' + ? wildCardKey.slice(0, -1) + : wildCardKey + }Id`, + splitArray[i], + ]); + } + } + const event_name = oddElemArray.join('_'); + return { + event_name: event_name, + wildCardObject: Object.fromEntries(wildCardArray), + }; +}