Compare commits

...
Sign in to create a new pull request.

11 commits

Author SHA1 Message Date
rjshrjndrn
7a19772575 chore(helm): Updating sourcemapreader image release 2023-11-14 11:33:15 +00:00
rjshrjndrn
07c60a0973 chore(helm): Updating peers image release 2023-11-14 11:26:20 +00:00
rjshrjndrn
c2b33ab9c0 chore(helm): Updating assist image release 2023-11-14 11:26:19 +00:00
rjshrjndrn
96fda9a217 chore(helm): Updating frontend image release 2023-11-14 11:21:22 +00:00
Shekar Siri
7af87215d6 fix(ui): header top right button alignment 2023-11-14 11:50:41 +01:00
Alexander
35e8558fd0 fix(assist): fixed metrics import in assist-ee 2023-11-14 11:37:43 +01:00
Alexander
a5743414f3 fix(assist): fixed metrics import in assist 2023-11-14 11:35:18 +01:00
Alexander
5cd12753d3 Assist clean up (#1654)
* feat(backend): moved http metrics into assist handlers file

* fix(assist): use correct value for requests_duration metric

* feat(assist): removed unnecessary imports

* fix(assist): fixed typo in repond method

* fix(assist): added metrics.js to clean script

* feat(assist): try to save http method asap in server logic

* feat(assist): final version

* feat(assist): removed prom-client from peers and sourcemap-reader

* feat(sourcemap-reader): added missing package-lock.json
2023-11-14 11:33:24 +01:00
Shekar Siri
fc839e99bb fix(ui): api delete request with empty params 2023-11-14 10:00:53 +01:00
rjshrjndrn
3c1adc6259 chore(helm): Updating frontend image release 2023-11-13 18:10:21 +00:00
Delirium
12c99bb7d7
move ios to separate repo (#1655)
* change(ios): podspec tagging

* change(ios): podspec tagging

* change(ios): podspec tagging

* change(ios): remove ios source to separate repo

* fix(ui): links to ios
2023-11-13 17:30:30 +01:00
55 changed files with 221 additions and 3302 deletions

View file

@ -3,7 +3,8 @@ const express = require('express');
const socket = require("./servers/websocket");
const {request_logger} = require("./utils/helper");
const health = require("./utils/health");
const assert = require('assert').strict;
const assert = require('assert').strict
const register = require('./utils/metrics').register;
const debug = process.env.debug === "1";
const heapdump = process.env.heapdump === "1";
@ -26,6 +27,15 @@ wsapp.get(['/', PREFIX, `${PREFIX}/`, `${PREFIX}/${P_KEY}`, `${PREFIX}/${P_KEY}/
wsapp.use(`${PREFIX}/${P_KEY}`, socket.wsRouter);
heapdump && wsapp.use(`${PREFIX}/${P_KEY}/heapdump`, dumps.router);
health.healthApp.get('/metrics', async (req, res) => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
} catch (ex) {
res.status(500).end(ex);
}
});
const wsserver = wsapp.listen(PORT, HOST, () => {
console.log(`WS App listening on http://${HOST}:${PORT}`);
health.healthApp.listen(health.PORT, HOST, health.listen_cb);

View file

@ -34,6 +34,7 @@ const wsRouter = express.Router();
const debug_log = process.env.debug === "1";
const socketsLive = async function (req, res) {
res.handlerName = 'socketsLive';
debug_log && console.log("[WS]looking for all available LIVE sessions");
let filters = await extractPayloadFromRequest(req, res);
let withFilters = hasFilters(filters);
@ -61,7 +62,7 @@ const socketsLive = async function (req, res) {
liveSessionsPerProject.forEach((sessions, projectId) => {
liveSessions[projectId] = Array.from(sessions);
});
respond(res, sortPaginate(liveSessions, filters));
respond(req, res, sortPaginate(liveSessions, filters));
}
wsRouter.get(`/sockets-list`, socketsList);

View file

@ -1,7 +1,6 @@
const express = require('express');
const HOST = process.env.LISTEN_HOST || '0.0.0.0';
const PORT = process.env.HEALTH_PORT || 8888;
const register = require('./metrics').register;
const {request_logger} = require("./helper");
const debug = process.env.debug === "1";
@ -39,15 +38,6 @@ healthApp.get('/shutdown', (req, res) => {
}
);
healthApp.get('/metrics', async (req, res) => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
} catch (ex) {
res.status(500).end(ex);
}
});
const listen_cb = async function () {
console.log(`Health App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');

View file

@ -1,8 +1,3 @@
const {
RecordRequestDuration,
IncreaseTotalRequests
} = require('../utils/metrics');
let PROJECT_KEY_LENGTH = parseInt(process.env.PROJECT_KEY_LENGTH) || 20;
let debug = process.env.debug === "1" || false;
@ -41,14 +36,8 @@ const extractPeerId = (peerId) => {
const request_logger = (identity) => {
return (req, res, next) => {
debug && console.log(identity, new Date().toTimeString(), 'REQUEST', req.method, req.originalUrl);
const startTs = performance.now(); // millis
req.startTs = performance.now(); // track request's start timestamp
res.on('finish', function () {
const duration = performance.now() - startTs;
let route = req.originalUrl.split('/')[3];
if (route !== undefined) {
IncreaseTotalRequests();
RecordRequestDuration(req.method, route, this.statusCode, duration);
}
if (this.statusCode !== 200 || debug) {
console.log(new Date().toTimeString(), 'RESPONSE', req.method, req.originalUrl, this.statusCode);
}

View file

@ -18,10 +18,14 @@ const {
const {
getServer
} = require('../utils/wsServer');
const {
RecordRequestDuration,
IncreaseTotalRequests
} = require('../utils/metrics');
const debug_log = process.env.debug === "1";
const respond = function (res, data) {
const respond = function (req, res, data) {
let result = {data}
if (process.env.uws !== "true") {
res.statusCode = 200;
@ -30,9 +34,13 @@ const respond = function (res, data) {
} else {
res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(JSON.stringify(result));
}
const duration = performance.now() - req.startTs;
IncreaseTotalRequests();
RecordRequestDuration(req.method.toLowerCase(), res.handlerName, 200, duration/1000.0);
}
const socketsList = async function (req, res) {
res.handlerName = 'socketsList';
let io = getServer();
debug_log && console.log("[WS]looking for all available sessions");
let filters = await extractPayloadFromRequest(req, res);
@ -60,10 +68,11 @@ const socketsList = async function (req, res) {
liveSessionsPerProject.forEach((sessions, projectId) => {
liveSessions[projectId] = Array.from(sessions);
});
respond(res, liveSessions);
respond(req, res, liveSessions);
}
const socketsListByProject = async function (req, res) {
res.handlerName = 'socketsListByProject';
let io = getServer();
debug_log && console.log("[WS]looking for available sessions");
let _projectKey = extractProjectKeyFromRequest(req);
@ -89,12 +98,13 @@ const socketsListByProject = async function (req, res) {
}
}
let sessions = Array.from(liveSessions);
respond(res, _sessionId === undefined ? sortPaginate(sessions, filters)
respond(req, res, _sessionId === undefined ? sortPaginate(sessions, filters)
: sessions.length > 0 ? sessions[0]
: null);
}
const socketsLiveByProject = async function (req, res) {
res.handlerName = 'socketsLiveByProject';
let io = getServer();
debug_log && console.log("[WS]looking for available LIVE sessions");
let _projectKey = extractProjectKeyFromRequest(req);
@ -129,16 +139,17 @@ const socketsLiveByProject = async function (req, res) {
}
}
let sessions = Array.from(liveSessions);
respond(res, _sessionId === undefined ? sortPaginate(sessions, filters) : sessions.length > 0 ? sessions[0] : null);
respond(req, res, _sessionId === undefined ? sortPaginate(sessions, filters) : sessions.length > 0 ? sessions[0] : null);
}
const socketsLiveBySession = async function (req, res) {
res.handlerName = 'socketsLiveBySession';
let io = getServer();
debug_log && console.log("[WS]looking for LIVE session");
let _projectKey = extractProjectKeyFromRequest(req);
let _sessionId = extractSessionIdFromRequest(req);
if (_sessionId === undefined) {
return respond(res, null);
return respond(req, res, null);
}
let filters = await extractPayloadFromRequest(req, res);
let withFilters = hasFilters(filters);
@ -166,10 +177,11 @@ const socketsLiveBySession = async function (req, res) {
}
let sessions = Array.from(liveSessions);
respond(res, sessions.length > 0 ? sessions[0] : null);
respond(req, res, sessions.length > 0 ? sessions[0] : null);
}
const autocomplete = async function (req, res) {
res.handlerName = 'autocomplete';
let io = getServer();
debug_log && console.log("[WS]autocomplete");
let _projectKey = extractProjectKeyFromRequest(req);
@ -189,7 +201,7 @@ const autocomplete = async function (req, res) {
}
}
}
respond(res, uniqueAutocomplete(results));
respond(req, res, uniqueAutocomplete(results));
}
module.exports = {

View file

@ -5,6 +5,7 @@ rm -rf ./utils/helper.js
rm -rf ./utils/assistHelper.js
rm -rf ./utils/httpHandlers.js
rm -rf ./utils/socketHandlers.js
rm -rf ./utils/metrics.js
rm -rf servers/peerjs-server.js
rm -rf servers/sourcemaps-handler.js

View file

@ -3,6 +3,16 @@ const {request_logger} = require('./utils/helper');
const express = require('express');
const health = require("./utils/health");
const assert = require('assert').strict;
const register = require('./utils/metrics').register;
health.healthApp.get('/metrics', async (req, res) => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
} catch (ex) {
res.status(500).end(ex);
}
});
let socket;
if (process.env.redis === "true") {
@ -59,14 +69,12 @@ if (process.env.uws !== "true") {
/* Either onAborted or simply finished request */
const onAbortedOrFinishedResponse = function (res, readStream) {
const onAbortedOrFinishedResponse = function (res) {
if (res.id === -1) {
debug && console.log("ERROR! onAbortedOrFinishedResponse called twice for the same res!");
} else {
debug && console.log('Stream was closed');
console.timeEnd(res.id);
readStream.destroy();
}
/* Mark this response already accounted for */
@ -76,8 +84,10 @@ if (process.env.uws !== "true") {
const uWrapper = function (fn) {
return (res, req) => {
res.id = 1;
req.startTs = performance.now(); // track request's start timestamp
req.method = req.getMethod();
res.onAborted(() => {
onAbortedOrFinishedResponse(res, readStream);
onAbortedOrFinishedResponse(res);
});
return fn(req, res);
}

View file

@ -40,6 +40,7 @@ let io;
const debug_log = process.env.debug === "1";
const socketsLive = async function (req, res) {
res.handlerName = 'socketsLive';
debug_log && console.log("[WS]looking for all available LIVE sessions");
let filters = await extractPayloadFromRequest(req, res);
let withFilters = hasFilters(filters);
@ -75,7 +76,7 @@ const socketsLive = async function (req, res) {
liveSessionsPerProject.forEach((sessions, projectId) => {
liveSessions[projectId] = Array.from(sessions);
});
respond(res, sortPaginate(liveSessions, filters));
respond(req, res, sortPaginate(liveSessions, filters));
}
wsRouter.get(`/sockets-list`, socketsList);

View file

@ -35,6 +35,7 @@ let io;
const debug_log = process.env.debug === "1";
const socketsLive = async function (req, res) {
res.handlerName = 'socketsLive';
debug_log && console.log("[WS]looking for all available LIVE sessions");
let filters = await extractPayloadFromRequest(req, res);
let withFilters = hasFilters(filters);
@ -62,7 +63,7 @@ const socketsLive = async function (req, res) {
liveSessionsPerProject.forEach((sessions, projectId) => {
liveSessions[projectId] = Array.from(sessions);
});
respond(res, sortPaginate(liveSessions, filters));
respond(req, res, sortPaginate(liveSessions, filters));
}
wsRouter.get(`/sockets-list`, socketsList);

View file

@ -22,5 +22,5 @@ MINIO_ACCESS_KEY = ''
MINIO_SECRET_KEY = ''
# APP and TRACKER VERSIONS
VERSION = 1.14.0
VERSION = 1.15.2
TRACKER_VERSION = '9.0.0'

View file

@ -127,7 +127,7 @@ export default class APIClient {
(this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`);
}
const init = this.getInit(method, options.clean ? clean(params) : params);
const init = this.getInit(method, options.clean && params ? clean(params) : params);
if (params !== undefined) {

View file

@ -68,7 +68,7 @@ function IdentifyUsersTab(props: Props) {
{platform.value === 'web' ? (
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
) : (
<HighlightCode className="swift" text={`ORTracker.shared.setUserID('john@doe.com');`} />
<HighlightCode className="swift" text={`OpenReplay.shared.setUserID('john@doe.com');`} />
)}
{platform.value === 'web' ? (
<div className="flex items-center my-2">
@ -119,7 +119,7 @@ function IdentifyUsersTab(props: Props) {
) : (
<HighlightCode
className="swift"
text={`ORTracker.shared.setMetadata('plan', 'premium');`}
text={`OpenReplay.shared.setMetadata('plan', 'premium');`}
/>
)}
</div>

View file

@ -5,9 +5,18 @@ import Highlight from 'react-highlight';
import CircleNumber from '../../CircleNumber';
import { CopyButton } from 'UI';
const installationCommand = 'add command after publishing!';
const installationCommand = `
// Cocoapods
pod 'Openreplay', '~> 1.0.5'
// Swift Package Manager
dependencies: [
.package(url: "https://github.com/openreplay/ios-tracker.git", from: "1.0.5"),
]
`;
const usageCode = `// AppDelegate.swift
import ORTracker
import OpenReplay
//...
@ -15,8 +24,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
ORTracker.shared.serverURL = "https://your.instance.com/ingest"
ORTracker.shared.start(projectKey: "PROJECT_KEY", options: .defaults)
OpenReplay.shared.serverURL = "https://your.instance.com/ingest"
OpenReplay.shared.start(projectKey: "PROJECT_KEY", options: .defaults)
// ...
return true
@ -30,7 +39,7 @@ let screen: Bool
let wifiOnly: Bool`;
const touches = `// SceneDelegate.Swift
import ORTracker
import OpenReplay
// ...
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
@ -46,14 +55,14 @@ import ORTracker
}
// ...`
const sensitive = `import ORTracker
const sensitive = `import OpenReplay
// swiftUI
Text("Very important sensitive text")
.sensitive()
// UIKit
ORTracker.shared.addIgnoredView(view)`
OpenReplay.shared.addIgnoredView(view)`
const inputs = `// swiftUI
TextField("Input", text: $text)

View file

@ -4,7 +4,6 @@ import Notifications from 'Components/Alerts/Notifications/Notifications';
import HealthStatus from 'Components/Header/HealthStatus';
import { getInitials } from 'App/utils';
import UserMenu from 'Components/Header/UserMenu/UserMenu';
import ErrorGenPanel from 'App/dev/components/ErrorGenPanel';
import { connect } from 'react-redux';
import { Popover, Space } from 'antd';
import ProjectDropdown from 'Shared/ProjectDropdown';
@ -20,7 +19,7 @@ function TopRight(props: Props) {
const { account } = props;
// @ts-ignore
return (
<Space className="flex items-center">
<Space style={{ lineHeight: '0'}}>
<ProjectDropdown />
<GettingStartedProgress />
@ -29,14 +28,12 @@ function TopRight(props: Props) {
{account.name ? <HealthStatus /> : null}
<Popover content={<UserMenu />} placement={'topRight'}>
<div className="flex items-center cursor-pointer">
<div className="w-10 h-10 bg-tealx rounded-full flex items-center justify-center color-white">
<div className='flex items-center cursor-pointer'>
<div className='bg-tealx rounded-full flex items-center justify-center color-white' style={{ width: '32px', height: '32px'}}>
{getInitials(account.name)}
</div>
</div>
</Popover>
{/*<ErrorGenPanel />*/}
</Space>
);
}

229
peers/package-lock.json generated
View file

@ -10,39 +10,30 @@
"license": "Elastic License 2.0 (ELv2)",
"dependencies": {
"express": "^4.18.2",
"peer": "^v1.0.1",
"prom-client": "^15.0.0"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz",
"integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==",
"engines": {
"node": ">=8.0.0"
"peer": "^v1.0.1"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
"integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@ -51,9 +42,9 @@
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.17.35",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz",
"integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==",
"version": "4.17.41",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz",
"integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
@ -61,48 +52,57 @@
"@types/send": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
},
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
},
"node_modules/@types/node": {
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.4.tgz",
"integrity": "sha512-ni5f8Xlf4PwnT/Z3f0HURc3ZSw8UyrqMqmM3L5ysa7VjHu8c3FOmIo1nKCcLrV/OAmtf3N4kFna/aJqxsfEtnA=="
"version": "20.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
"version": "6.9.10",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
"integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw=="
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
},
"node_modules/@types/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz",
"integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==",
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz",
"integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
"integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
"dependencies": {
"@types/http-errors": "*",
"@types/mime": "*",
"@types/node": "*"
}
},
"node_modules/@types/ws": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
"integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz",
"integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==",
"dependencies": {
"@types/node": "*"
}
@ -146,11 +146,6 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/bintrees": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@ -183,12 +178,13 @@
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -283,6 +279,19 @@
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -447,9 +456,12 @@
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
@ -460,28 +472,39 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"function-bind": "^1.1.1"
"get-intrinsic": "^1.1.3"
},
"engines": {
"node": ">= 0.4.0"
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
@ -506,6 +529,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -636,9 +670,9 @@
}
},
"node_modules/node-fetch": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz",
"integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
@ -661,9 +695,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -716,18 +750,6 @@
"url": "https://opencollective.com/peer"
}
},
"node_modules/prom-client": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.0.0.tgz",
"integrity": "sha512-UocpgIrKyA2TKLVZDSfm8rGkL13C19YrQBAiG3xo3aDFWcHedxRxI3z+cIcucoxpSO0h5lff5iv/SXoxyeopeA==",
"dependencies": {
"@opentelemetry/api": "^1.4.0",
"tdigest": "^0.1.1"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -850,6 +872,20 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -900,14 +936,6 @@
"node": ">=8"
}
},
"node_modules/tdigest": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
"dependencies": {
"bintrees": "1.0.2"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -928,6 +956,11 @@
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -977,9 +1010,9 @@
}
},
"node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"engines": {
"node": ">=10.0.0"
},

View file

@ -19,7 +19,6 @@
"homepage": "https://github.com/openreplay/openreplay#readme",
"dependencies": {
"express": "^4.18.2",
"peer": "^v1.0.1",
"prom-client": "^15.0.0"
"peer": "^v1.0.1"
}
}

View file

@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.1
version: 0.1.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.15.0"
AppVersion: "v1.15.1"

View file

@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (frontends://semver.org/)
version: 0.1.10
version: 0.1.12
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.15.0"
AppVersion: "v1.15.2"

View file

@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.1
version: 0.1.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.15.0"
AppVersion: "v1.15.1"

View file

@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (sourcemapreaders://semver.org/)
version: 0.1.1
version: 0.1.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.15.0"
AppVersion: "v1.15.1"

View file

@ -12,7 +12,6 @@
"@azure/storage-blob": "^12.17.0",
"aws-sdk": "^2.1493.0",
"express": "^4.18.2",
"prom-client": "^15.0.0",
"source-map": "^0.7.4"
}
},
@ -234,9 +233,9 @@
}
},
"node_modules/aws-sdk": {
"version": "2.1493.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1493.0.tgz",
"integrity": "sha512-fnOoakH7HUHWDPjnXuCoy3KR+4Fn+Z76ZZjLUbsljzOdy1aBiN5C+cnrEnV8wWKI1dUjU6C1nwEystzi50HHyA==",
"version": "2.1495.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1495.0.tgz",
"integrity": "sha512-JbefhY9G3WooJJjTtSUegyuNiYhY0vFd0q1KtpY8W+z1U6aKovkIyLJsR2de6u8KXZQkcwT+7N46BYT1SbZ5sQ==",
"dependencies": {
"buffer": "4.9.2",
"events": "1.1.1",
@ -272,11 +271,6 @@
}
]
},
"node_modules/bintrees": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@ -541,13 +535,12 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
},
"funding": {
@ -576,17 +569,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
@ -850,18 +832,6 @@
"node": ">= 0.6.0"
}
},
"node_modules/prom-client": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.0.0.tgz",
"integrity": "sha512-UocpgIrKyA2TKLVZDSfm8rGkL13C19YrQBAiG3xo3aDFWcHedxRxI3z+cIcucoxpSO0h5lff5iv/SXoxyeopeA==",
"dependencies": {
"@opentelemetry/api": "^1.4.0",
"tdigest": "^0.1.1"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -1029,14 +999,6 @@
"node": ">= 0.8"
}
},
"node_modules/tdigest": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
"dependencies": {
"bintrees": "1.0.2"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View file

@ -21,7 +21,6 @@
"@azure/storage-blob": "^12.17.0",
"aws-sdk": "^2.1493.0",
"express": "^4.18.2",
"prom-client": "^15.0.0",
"source-map": "^0.7.4"
}
}

View file

@ -1,3 +0,0 @@
.build
.swiftpm
.DS_Store

View file

@ -1,44 +0,0 @@
Elastic License 2.0 (ELv2)
**Acceptance**
By using the software, you agree to all of the terms and conditions below.
**Copyright License**
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below
**Limitations**
You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensors trademarks is subject to applicable law.
**Patents**
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
**Notices**
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
**No Other Rights**
These terms do not imply any licenses other than those expressly granted in these terms.
**Termination**
If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
**No Liability**
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
**Definitions**
The *licensor* is the entity offering these terms, and the *software* is the software the licensor makes available under these terms, including any portion of it.
*you* refers to the individual or entity agreeing to these terms.
*your company* is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. *control* means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
*your licenses* are all the licenses granted to you for the software under these terms.
*use* means anything you do with the software requiring one of your licenses.
*trademark* means trademarks, service marks, and similar rights.

View file

@ -1,14 +0,0 @@
Pod::Spec.new do |s|
s.name = 'ORTracker'
s.version = '0.1.0'
s.summary = 'A short description of ORTracker.'
s.homepage = 'https://github.com/openreplay/openreplay/tracker/tracker-ios'
s.license = { :type => 'ELv2', :file => 'LICENSE.md' }
s.author = { 'Nick Delirium' => 'nick.delirium@proton.me' }
s.source = { :git => 'https://github.com/openreplay/openreplay/tracker/tracker-ios', :tag => s.version.to_s }
s.ios.deployment_target = '13.0'
s.swift_version = '5.0'
s.source_files = 'Sources/ORTracker/**/*'
s.dependency 'SWCompression'
s.dependency 'DeviceKit'
end

View file

@ -1,32 +0,0 @@
{
"pins" : [
{
"identity" : "bitbytedata",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tsolomko/BitByteData",
"state" : {
"revision" : "36df26fe4586b4f23d76cfd8b47076998343a2b2",
"version" : "2.0.3"
}
},
{
"identity" : "devicekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/devicekit/DeviceKit.git",
"state" : {
"revision" : "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6",
"version" : "4.9.0"
}
},
{
"identity" : "swcompression",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tsolomko/SWCompression.git",
"state" : {
"revision" : "cd39ca0a3b269173bab06f68b182b72fa690765c",
"version" : "4.8.5"
}
}
],
"version" : 2
}

View file

@ -1,38 +0,0 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ORTracker",
platforms: [
.iOS(.v13)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "ORTracker",
targets: ["ORTracker"]
),
],
dependencies: [
.package(url: "https://github.com/devicekit/DeviceKit.git", from: "4.0.0"),
.package(url: "https://github.com/tsolomko/SWCompression.git", .upToNextMajor(from: "4.8.5")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "ORTracker",
dependencies: [
.product(name: "SWCompression", package: "SWCompression"),
.product(name: "DeviceKit", package: "DeviceKit"),
]
),
.testTarget(
name: "ORTrackerTests",
dependencies: ["ORTracker"]
),
]
)

View file

@ -1,8 +0,0 @@
platform :ios, '13.0'
target 'ORTracker' do
use_frameworks!
pod 'SWCompression', '~> 4.8'
pod 'DeviceKit', '~> 5.1'
end

View file

@ -1,89 +0,0 @@
Setting up tracker
```swift
// AppDelegate.swift
import ORTracker
//...
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
ORTracker.shared.serverURL = "https://your.instance.com/ingest"
ORTracker.shared.start(projectKey: "projectkey", options: .defaults)
// ...
return true
}
```
Options (default all `true`)
```swift
let crashes: Bool
let analytics: Bool
let performances: Bool
let logs: Bool
let screen: Bool
let wifiOnly: Bool
```
Setting up touches listener
```swift
// SceneDelegate.Swift
import ORTracker
// ...
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
.environmentObject(TodoStore())
if let windowScene = scene as? UIWindowScene {
let window = TouchTrackingWindow(windowScene: windowScene) // <<<< here
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
```
Adding sensitive views (will be blurred in replay)
```swift
import ORTracker
// swiftUI
Text("Very important sensitive text")
.sensitive()
// UIKit
ORTracker.shared.addIgnoredView(view)
```
Adding tracked inputs
```swift
// swiftUI
TextField("Input", text: $text)
.observeInput(text: $text, label: "tracker input #1", masked: Bool)
// UIKit will use placeholder as label and sender.isSecureTextEntry to mask the input
Analytics.shared.addObservedInput(inputEl)
```
Observing views
```swift
// swiftUI
TextField("Test")
.observeView(title: "Screen title", viewName: "test input name")
// UIKit
Analytics.shared.addObservedView(view: inputEl, title: "Screen title", viewName: "test input name")
```
will send IOSScreenEnter and IOSScreenLeave when view appears/dissapears on/from screen

View file

@ -1,230 +0,0 @@
import UIKit
import CommonCrypto
protocol NSObjectCoding: NSCoding, NSObject {}
extension NSObjectCoding {
static func from(data: Data, offset: inout Int) throws -> Self {
let valueData = try data.readData(offset: &offset)
guard let result = try NSKeyedUnarchiver.unarchivedObject(ofClass: self, from: valueData)
else { throw NSError(domain: "ErrorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error reading NSCoding"]) }
return result
}
}
extension Data {
mutating func appendString(_ string: String) {
if let data = string.data(using: .utf8) {
append(data)
}
}
func sha256() -> String {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
self.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash)
}
return Data(hash).hexEncodedString()
}
func hexEncodedString() -> String {
return map { String(format: "%02hhx", $0) }.joined()
}
func subdata(start: Int, length: Int) -> Data? {
let start = startIndex.advanced(by: start)
let end = start.advanced(by: length)
guard start >= 0, end <= count else { return nil }
return subdata(in: start..<end)
}
}
extension Data {
func readPrimary<T>(offset: inout Int) throws -> T {
if T.self == CGFloat.self {
return CGFloat(try readPrimary(offset: &offset) as Double) as! T
}
if T.self == UInt64.self {
return try readUint(offset: &offset) as! T
}
if T.self == Int64.self {
return try readInt(offset: &offset) as! T
}
if T.self == Bool.self {
return try readBoolean(offset: &offset) as! T
}
let valueSize = MemoryLayout<T>.size
guard let data = subdata(start: offset, length: valueSize) else { throw "Error reading primary value" }
let result = data.withUnsafeBytes {
$0.load(as: T.self)
}
offset += data.count
return result
}
func readData(offset: inout Int) throws -> Data {
let length = try readUint(offset: &offset)
guard let data = subdata(start: offset, length: Int(length)),
length == data.count else { throw "Error reading data" }
offset += Int(length)
return data
}
func readString(offset: inout Int) throws -> String {
let data = try readData(offset: &offset)
guard let result = String(data: data, encoding: .utf8)
else { throw "Error reading string" }
return result
}
private func readByte(offset: inout Int) throws -> UInt8 {
guard offset < count else { throw "Error reading byte" }
let b = self[offset]
offset += 1
return b
}
private func readUint(offset: inout Int) throws -> UInt64 {
var x: UInt64 = 0
var s: Int = 0
var i: Int = 0
while true {
let b = try readByte(offset: &offset)
if b < 0x80 {
if i > 9 || i == 9 && b > 1 {
throw "Invalid UInt"
}
return x | UInt64(b)<<s
}
x |= UInt64(b&0x7f) << s
s += 7
i += 1
}
}
private func readInt(offset: inout Int) throws -> Int64 {
let ux = try readUint(offset: &offset)
var x = Int64(ux >> 1)
if ux&1 != 0 {
x = ~x
}
return x
}
private func readBoolean(offset: inout Int) throws -> Bool {
return try readByte(offset: &offset) == 1
}
}
extension Data {
init(value: Any?) {
self.init()
if let value = value {
writeValue(value: value)
}
}
init(values: Any...) {
self.init()
values.forEach { writeValue(value: $0) }
}
mutating func writeValues(values: Any...) {
values.forEach { writeValue(value: $0) }
}
mutating func writeValue(value: Any) {
let oldLength = count
switch value {
case is NSNull: break
case let parsed as Data: writeData(parsed, sizePrefix: true)
case let parsed as Int64: writeInt(parsed)
case let parsed as UInt64: writeUint(parsed)
case let parsed as Int: writePrimary(parsed)
case let parsed as UInt8: writePrimary(parsed)
case let parsed as UInt16: writePrimary(parsed)
case let parsed as UInt32: writePrimary(parsed)
case let parsed as Float: writePrimary(parsed)
case let parsed as CGFloat: writePrimary(Double(parsed))
case let parsed as Double: writePrimary(parsed)
case let parsed as Bool: writeBoolean(parsed)
case let parsed as UIEdgeInsets:
writeValues(values: parsed.top, parsed.bottom, parsed.left, parsed.right)
case let parsed as CGRect:
writeValues(values: parsed.origin.x, parsed.origin.y, parsed.size.width, parsed.size.height)
case let parsed as CGPoint: writeValues(values: parsed.x, parsed.y)
case let parsed as CGSize: writeValues(values: parsed.width, parsed.height)
case let parsed as UIColor:
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
parsed.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
writeValues(values: red, green, blue, alpha)
case let parsed as String: writeString(parsed)
case let parsed as UIFont: writeNSCoding(parsed.fontDescriptor)
case let parsed as NSAttributedString: writeNSCoding(parsed)
default: break
}
if CFGetTypeID(value as CFTypeRef) == CGColor.typeID {
writeValue(value: UIColor(cgColor: value as! CGColor))
}
let length = count - oldLength
if length == 0 && !(value is NSNull) {
print("Nothing was written for \(String(describing: type(of: value))):\(value) ")
}
}
private mutating func writePrimary<T>(_ value: T) {
writeData(Swift.withUnsafeBytes(of: value) { Data($0) }, sizePrefix: false)
}
private mutating func writeString(_ string: String) {
let stringData = string.data(using: .utf8, allowLossyConversion: true) ?? Data()
writeData(stringData, sizePrefix: true)
}
private mutating func writeNSCoding(_ coding: NSCoding) {
do {
let valueData = try NSKeyedArchiver.archivedData(withRootObject: coding, requiringSecureCoding: true)
writeData(valueData, sizePrefix: true)
} catch {
print("Unexpected error: \(error).")
}
}
private mutating func writeData(_ data: Data, sizePrefix: Bool) {
if sizePrefix {
writeValue(value: UInt64(data.count))
}
append(data)
}
private mutating func writeUint(_ input: UInt64) {
var v = input
while v >= 0x80 {
append(UInt8(v.littleEndian & 0x7F) | 0x80) // v.littleEndian ?
v >>= 7
}
append(UInt8(v))
}
private mutating func writeInt(_ v: Int64) {
var uv = UInt64(v) << 1
if v < 0 {
uv = ~uv
}
writeUint(uv)
}
private mutating func writeBoolean(_ v: Bool) {
append(v ? 1 : 0)
}
}
extension Encodable {
func toJSONData() -> Data? { try? JSONEncoder().encode(self) }
}
extension String: Error {}

View file

@ -1,17 +0,0 @@
import Foundation
extension Date {
func format(_ format: String) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.dateFormat = format
return formatter.string(from: self)
}
static func parse(dateStr: String, format: String) -> Date? {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.dateFormat = format
return formatter.date(from: dateStr)
}
}

View file

@ -1,10 +0,0 @@
import Foundation
extension String {
func contains(regex: String) -> Bool {
guard let regex = try? NSRegularExpression(pattern: regex) else { return false }
let range = NSRange(location: 0, length: self.utf16.count)
return regex.firstMatch(in: self, options: [], range: range) != nil
}
}

View file

@ -1,24 +0,0 @@
import UIKit
extension UIImage {
func applyBlurWithRadius(_ blurRadius: CGFloat) -> UIImage? {
if (size.width < 1 || size.height < 1) {
return nil
}
guard let inputCGImage = self.cgImage else {
return nil
}
let inputImage = CIImage(cgImage: inputCGImage)
let filter = CIFilter(name: "CIGaussianBlur")
filter?.setValue(inputImage, forKey: kCIInputImageKey)
filter?.setValue(blurRadius, forKey: kCIInputRadiusKey)
guard let outputImage = filter?.outputImage else {
return nil
}
let context = CIContext(options: nil)
guard let outputCGImage = context.createCGImage(outputImage, from: inputImage.extent) else {
return nil
}
return UIImage(cgImage: outputCGImage)
}
}

View file

@ -1,26 +0,0 @@
import UIKit
private var viewCounter = 0
private var shortIds = [String: String]()
extension UIView: Sanitizable {
public var identifier: String {
let longId = longIdentifier
if let existingId = shortIds[longId] {
return existingId
}
let shortId = "\(viewCounter)"
viewCounter += 1
shortIds[longId] = shortId
return shortId
}
public var longIdentifier: String {
return String(describing: type(of: self)) + "-" + Unmanaged.passUnretained(self).toOpaque().debugDescription
}
public var frameInWindow: CGRect? {
return self.window == nil ? nil : self.convert(self.bounds, to: self.window)
}
}

View file

@ -1,288 +0,0 @@
import UIKit
import SwiftUI
import Combine
import ObjectiveC
open class Analytics: NSObject {
public static let shared = Analytics()
public var enabled = false
public var observedInputs: [UITextField] = []
public var observedViews: [UIView] = []
private override init() {
super.init()
}
public func start() {
enabled = true
UIViewController.swizzleLifecycleMethods()
}
public func stop() {
observedViews.removeAll()
observedInputs.removeAll()
enabled = false
// Unswizzle (reverse the swizzling) if needed in the
}
@objc private func handleTap(gesture: UITapGestureRecognizer) {
let location = gesture.location(in: nil)
DebugUtils.log("Tap detected at: \(location)")
}
@objc public func addObservedInput(_ element: UITextField) {
observedInputs.append(element)
element.addTarget(self, action: #selector(textInputFinished), for: .editingDidEnd)
}
@objc public func addObservedView(view: UIView, screenName: String, viewName: String) {
view.orScreenName = screenName
view.orViewName = viewName
observedViews.append(view)
}
@objc public func sendClick(label: String, x: UInt64, y: UInt64) {
let message = ORIOSClickEvent(label: label, x: x, y: y)
if Analytics.shared.enabled {
MessageCollector.shared.sendMessage(message)
}
}
@objc public func sendSwipe(label: String, x: UInt64, y: UInt64, direction: String) {
let message = ORIOSSwipeEvent(label: label, x: x,y: y, direction: direction)
if Analytics.shared.enabled {
MessageCollector.shared.sendMessage(message)
}
}
@objc func textInputFinished(_ sender: UITextField) {
#if DEBUG
DebugUtils.log(">>>>>Text finish \(sender.text ?? "no_text") \(sender.placeholder ?? "no_placeholder")")
#endif
var sentText = sender.text
if sender.isSecureTextEntry {
sentText = "***"
}
MessageCollector.shared.sendMessage(ORIOSInputEvent(value: sentText ?? "", valueMasked: sender.isSecureTextEntry, label: sender.placeholder ?? ""))
}
}
extension UIViewController {
static func swizzleLifecycleMethods() {
DebugUtils.log(">>>>> ORTracker: swizzle UIViewController")
Self.swizzle(original: #selector(viewDidAppear(_:)), swizzled: #selector(swizzledViewDidAppear(_:)))
Self.swizzle(original: #selector(viewDidDisappear(_:)), swizzled: #selector(swizzledViewDidDisappear(_:)))
}
static private func swizzle(original: Selector, swizzled: Selector) {
if let originalMethod = class_getInstanceMethod(self, original),
let swizzledMethod = class_getInstanceMethod(self, swizzled) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
@objc private func swizzledViewDidAppear(_ animated: Bool) {
self.swizzledViewDidAppear(animated)
if let (screenName, viewName) = isViewOrSubviewObservedEnter() {
let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: true)
if Analytics.shared.enabled {
MessageCollector.shared.sendMessage(message)
}
}
}
@objc private func swizzledViewDidDisappear(_ animated: Bool) {
self.swizzledViewDidDisappear(animated)
if let (screenName, viewName) = isViewOrSubviewObservedEnter() {
let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: false)
if Analytics.shared.enabled {
MessageCollector.shared.sendMessage(message)
}
}
}
private func isViewOrSubviewObservedEnter() -> (screenName: String, viewName: String)? {
var viewsToCheck: [UIView] = [self.view]
while !viewsToCheck.isEmpty {
let view = viewsToCheck.removeFirst()
if let observed = Analytics.shared.observedViews.first(where: { $0 == view }) {
let screenName = observed.orScreenName ?? "Unknown ScreenName"
let viewName = observed.orViewName ?? "Unknown View"
return (screenName, viewName)
}
viewsToCheck.append(contentsOf: view.subviews)
}
return nil
}
}
public class TouchTrackingWindow: UIWindow {
var touchStart: CGPoint?
public override func sendEvent(_ event: UIEvent) {
super.sendEvent(event)
guard let touches = event.allTouches else { return }
for touch in touches {
switch touch.phase {
case .began:
touchStart = touch.location(in: self)
case .ended:
let location = touch.location(in: self)
let isSwipe = touchStart!.distance(to: location) > 10
var event: ORMessage
let description = getViewDescription(touch.view) ?? "UIView"
if isSwipe {
DebugUtils.log("Swipe from \(touchStart ?? CGPoint(x: 0, y: 0)) to \(location)")
event = ORIOSSwipeEvent(label: description, x: UInt64(location.x),y: UInt64(location.y), direction: detectSwipeDirection(from: touchStart!, to: location))
} else {
event = ORIOSClickEvent(label: description, x: UInt64(location.x), y: UInt64(location.y))
DebugUtils.log("Touch from \(touchStart ?? CGPoint(x: 0, y: 0)) to \(location)")
}
touchStart = nil
MessageCollector.shared.sendMessage(event)
default:
break
}
}
}
private func getViewDescription(_ view: UIView?) -> String? {
guard let view = view else {
return nil
}
if let textField = view as? UITextField {
return "UITextField '\(textField.placeholder ?? "No Placeholder")'"
} else if let label = view as? UILabel {
return "UILabel '\(label.text ?? "No Text")'"
} else if let button = view as? UIButton {
return "UIButton '\(button.currentTitle ?? "No Title")'"
} else if let textView = view as? UITextView {
return "UITextView '\(textView.text ?? "No Text")'"
} else {
return "\(type(of: view))"
}
}
private func detectSwipeDirection(from start: CGPoint, to end: CGPoint) -> String {
let deltaX = end.x - start.x
let deltaY = end.y - start.y
if abs(deltaX) > abs(deltaY) {
if deltaX > 0 {
return "right"
} else {
return "left"
}
} else if abs(deltaY) > abs(deltaX) {
if deltaY > 0 {
return "down"
} else {
return "up"
}
}
return "right"
}
}
extension CGPoint {
func distance(to point: CGPoint) -> CGFloat {
return hypot(point.x - x, point.y - y)
}
}
public struct ObservedInputModifier: ViewModifier {
@Binding var text: String
let label: String?
let masked: Bool?
public func body(content: Content) -> some View {
content
.onReceive(text.publisher.collect()) { value in
let stringValue = String(value)
textInputFinished(value: stringValue, label: label, masked: masked)
}
}
private func textInputFinished(value: String, label: String?, masked: Bool?) {
guard !value.isEmpty else { return }
var sentValue = value
if masked ?? false {
sentValue = "****"
}
MessageCollector.shared.sendDebouncedMessage(ORIOSInputEvent(value: sentValue, valueMasked: masked ?? false, label: label ?? ""))
}
}
public struct ViewLifecycleModifier: ViewModifier {
let screenName: String
let viewName: String
public func body(content: Content) -> some View {
content
.onAppear {
DebugUtils.log("<><><>view appear \(viewName)")
let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: true)
if Analytics.shared.enabled {
MessageCollector.shared.sendMessage(message)
}
}
.onDisappear {
DebugUtils.log("<><><>disappear view \(viewName)")
let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: false)
if Analytics.shared.enabled {
MessageCollector.shared.sendMessage(message)
}
}
}
}
public extension View {
func observeView(screenName: String, viewName: String) -> some View {
self.modifier(ViewLifecycleModifier(screenName: screenName, viewName: viewName))
}
func observeInput(text: Binding<String>, label: String?, masked: Bool?) -> some View {
self.modifier(ObservedInputModifier(text: text, label: label, masked: masked))
}
}
extension UIView {
private struct AssociatedKeys {
static var orScreenName: String = "OR: screenName"
static var orViewName: String = "OR: viewName"
}
var orScreenName: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.orScreenName) as? String
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.orScreenName, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var orViewName: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.orViewName) as? String
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.orViewName, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}

View file

@ -1,64 +0,0 @@
import UIKit
public class Crashs: NSObject {
public static let shared = Crashs()
private static var fileUrl: URL? = nil
private var isActive = false
private override init() {
Crashs.fileUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("ASCrash.dat")
if let fileUrl = Crashs.fileUrl,
FileManager.default.fileExists(atPath: fileUrl.path),
let crashData = try? Data(contentsOf: fileUrl) {
NetworkManager.shared.sendLateMessage(content: crashData) { (success) in
guard success else { return }
if FileManager.default.fileExists(atPath: fileUrl.path) {
try? FileManager.default.removeItem(at: fileUrl)
}
}
}
}
public func start() {
NSSetUncaughtExceptionHandler { (exception) in
print("<><> captured crash \(exception)")
let message = ORIOSCrash(name: exception.name.rawValue,
reason: exception.reason ?? "",
stacktrace: exception.callStackSymbols.joined(separator: "\n"))
let messageData = message.contentData()
if let fileUrl = Crashs.fileUrl {
try? messageData.write(to: fileUrl)
}
NetworkManager.shared.sendMessage(content: messageData) { (success) in
guard success else { return }
if let fileUrl = Crashs.fileUrl,
FileManager.default.fileExists(atPath: fileUrl.path) {
try? FileManager.default.removeItem(at: fileUrl)
}
}
}
isActive = true
}
public func sendLateError(exception: NSException) {
let message = ORIOSCrash(name: exception.name.rawValue,
reason: exception.reason ?? "",
stacktrace: exception.callStackSymbols.joined(separator: "\n")
)
NetworkManager.shared.sendLateMessage(content: message.contentData()) { (success) in
guard success else { return }
if let fileUrl = Crashs.fileUrl,
FileManager.default.fileExists(atPath: fileUrl.path) {
try? FileManager.default.removeItem(at: fileUrl)
}
}
}
public func stop() {
if isActive {
NSSetUncaughtExceptionHandler(nil)
isActive = false
}
}
}

View file

@ -1,47 +0,0 @@
import UIKit
class LogsListener: NSObject {
static let shared = LogsListener()
private let outputListener = Listener(fileHandle: FileHandle.standardOutput, severity: "info")
private let errorListener = Listener(fileHandle: FileHandle.standardError, severity: "error")
func start() {
outputListener.start()
errorListener.start()
}
class Listener: NSObject {
let inputPipe = Pipe()
let outputPipe = Pipe()
let fileHandle: FileHandle
let severity: String
init(fileHandle: FileHandle, severity: String) {
self.fileHandle = fileHandle
self.severity = severity
super.init()
inputPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
guard let strongSelf = self else { return }
let data = fileHandle.availableData
if let string = String(data: data, encoding: String.Encoding.utf8) {
let message = ORIOSLog(severity: severity, content: string)
MessageCollector.shared.sendMessage(message)
}
strongSelf.outputPipe.fileHandleForWriting.write(data)
}
}
func start() {
dup2(fileHandle.fileDescriptor, outputPipe.fileHandleForWriting.fileDescriptor)
dup2(inputPipe.fileHandleForWriting.fileDescriptor, fileHandle.fileDescriptor)
}
func stop() {
}
}
}

View file

@ -1,150 +0,0 @@
import UIKit
open class NetworkListener: NSObject {
private let startTime: UInt64
private var url: String = ""
private var method: String = ""
private var requestBody: String?
private var requestHeaders: [String: String]?
var ignoredKeys = ["password"]
var ignoredHeaders = ["Authentication", "Auth"]
public override init() {
startTime = UInt64(Date().timeIntervalSince1970 * 1000)
}
public convenience init(request: URLRequest) {
self.init()
start(request: request)
}
public convenience init(task: URLSessionTask) {
self.init()
start(task: task)
}
open func start(request: URLRequest) {
url = request.url?.absoluteString ?? ""
method = request.httpMethod ?? "GET"
requestHeaders = request.allHTTPHeaderFields
if let body = request.httpBody {
requestBody = String(data: body, encoding: .utf8)
} else {
requestBody = ""
DebugUtils.log("error getting request body (start request)")
}
}
open func start(task: URLSessionTask) {
if let request = task.currentRequest {
start(request: request)
} else {
DebugUtils.log("error getting request body (start task)")
}
}
open func finish(response: URLResponse?, data: Data?) {
let endTime = UInt64(Date().timeIntervalSince1970 * 1000)
let httpResponse = response as? HTTPURLResponse
var responseBody: String? = nil
if let data = data {
responseBody = String(data: data, encoding: .utf8)
} else {
DebugUtils.log("error getting request body (finish)")
}
let requestContent: [String: Any?] = [
"body": sanitizeBody(body: requestBody),
"headers": sanitizeHeaders(headers: requestHeaders)
]
var responseContent: [String: Any?]
if let httpResponse = httpResponse {
let headers = transformHeaders(httpResponse.allHeaderFields)
responseContent = [
"body": sanitizeBody(body: responseBody),
"headers": sanitizeHeaders(headers: headers)
]
} else {
responseContent = [
"body": "",
"headers": ""
]
}
let requestJSON = convertDictionaryToJSONString(dictionary: requestContent) ?? ""
let responseJSON = convertDictionaryToJSONString(dictionary: responseContent) ?? ""
let status = httpResponse?.statusCode ?? 0
let message = ORIOSNetworkCall(
type: "request",
method: method,
URL: url,
request: requestJSON,
response: responseJSON,
status: UInt64(status),
duration: endTime - startTime
)
MessageCollector.shared.sendMessage(message)
}
private func sanitizeHeaders(headers: [String: String]?) -> [String: String]? {
guard let headerContent = headers else { return nil }
var sanitizedHeaders = headerContent
for key in ignoredKeys {
if sanitizedHeaders.keys.contains(key) {
sanitizedHeaders[key] = "***"
}
}
return sanitizedHeaders
}
private func sanitizeBody(body: String?) -> String? {
guard let bodyContent = body else { return nil }
var sanitizedBody = bodyContent
for key in ignoredKeys {
if let range = sanitizedBody.range(of: "\"\(key)\":\"[^\"]*\"", options: .regularExpression) {
sanitizedBody.replaceSubrange(range, with: "\"\(key)\":\"***\"")
}
}
return sanitizedBody
}
}
func convertDictionaryToJSONString(dictionary: [String: Any?]) -> String? {
if let jsonData = try? JSONSerialization.data(withJSONObject: dictionary, options: []) {
return String(data: jsonData, encoding: .utf8)
}
return nil
}
func transformHeaders(_ headers: [AnyHashable: Any]) -> [String: String] {
var stringHeaders: [String: String] = [:]
for (key, value) in headers {
if let stringKey = key.base as? String, let stringValue = value as? String {
stringHeaders[stringKey] = stringValue
}
}
return stringHeaders
}
func isJSONString(string: String) -> Bool {
if let data = string.data(using: .utf8) {
do {
_ = try JSONSerialization.jsonObject(with: data, options: [])
return true
} catch {
DebugUtils.log("Error: \(error)")
return false
}
}
return false
}

View file

@ -1,177 +0,0 @@
import UIKit
open class PerformanceListener: NSObject {
public static let shared = PerformanceListener()
private var cpuTimer: Timer?
private var cpuIteration = 0
private var memTimer: Timer?
public var isActive = false
func start() {
#warning("Can interfere with client usage")
UIDevice.current.isBatteryMonitoringEnabled = true
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
let observe: (Notification.Name) -> Void = {
NotificationCenter.default.addObserver(self, selector: #selector(self.notified(_:)), name: $0, object: nil)
}
observe(.NSBundleResourceRequestLowDiskSpace)
observe(.NSProcessInfoPowerStateDidChange)
observe(ProcessInfo.thermalStateDidChangeNotification)
observe(UIApplication.didReceiveMemoryWarningNotification)
observe(UIDevice.batteryLevelDidChangeNotification)
observe(UIDevice.batteryStateDidChangeNotification)
observe(UIDevice.orientationDidChangeNotification)
getCpuMessage()
getMemoryMessage()
cpuTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { (_) in
self.getCpuMessage()
})
memTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true, block: { (_) in
self.getMemoryMessage()
})
isActive = true
NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(resume), name: UIApplication.willEnterForegroundNotification, object: nil)
}
@objc func resume() {
#if DEBUG
DebugUtils.log("Resume")
#endif
getCpuMessage()
getMemoryMessage()
MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "background", value: UInt64(0)))
}
@objc func pause() {
#if DEBUG
DebugUtils.log("Background")
#endif
MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "background", value: UInt64(1)))
}
func getCpuMessage() {
if let cpu = self.cpuUsage() {
MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "mainThreadCPU", value: UInt64(cpu)))
}
}
func getMemoryMessage() {
if let mem = self.memoryUsage() {
MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "memoryUsage", value: UInt64(mem)))
}
}
func stop() {
if isActive {
UIDevice.current.isBatteryMonitoringEnabled = false
UIDevice.current.endGeneratingDeviceOrientationNotifications()
NotificationCenter.default.removeObserver(self, name: .NSBundleResourceRequestLowDiskSpace, object: nil)
NotificationCenter.default.removeObserver(self, name: .NSProcessInfoPowerStateDidChange, object: nil)
NotificationCenter.default.removeObserver(self, name: ProcessInfo.thermalStateDidChangeNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIDevice.batteryLevelDidChangeNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIDevice.batteryStateDidChangeNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
cpuTimer?.invalidate()
cpuTimer = nil
memTimer?.invalidate()
memTimer = nil
isActive = false
}
}
public func sendBattery() {
let message = ORIOSPerformanceEvent(name: "batteryLevel", value: 20)
MessageCollector.shared.sendMessage(message)
}
public func sendThermal() {
let message2 = ORIOSPerformanceEvent(name: "thermalState", value: 2)
MessageCollector.shared.sendMessage(message2)
}
@objc func notified(_ notification: Notification) {
var message: ORIOSPerformanceEvent? = nil
switch notification.name {
case .NSBundleResourceRequestLowDiskSpace:
message = ORIOSPerformanceEvent(name: "lowDiskSpace", value: 0)
case .NSProcessInfoPowerStateDidChange:
message = ORIOSPerformanceEvent(name: "isLowPowerModeEnabled", value: ProcessInfo.processInfo.isLowPowerModeEnabled ? 1 : 0)
case ProcessInfo.thermalStateDidChangeNotification:
message = ORIOSPerformanceEvent(name: "thermalState", value: UInt64(ProcessInfo.processInfo.thermalState.rawValue))
case UIApplication.didReceiveMemoryWarningNotification:
message = ORIOSPerformanceEvent(name: "memoryWarning", value: 0)
case UIDevice.batteryLevelDidChangeNotification:
message = ORIOSPerformanceEvent(name: "batteryLevel", value: UInt64(max(0.0, UIDevice.current.batteryLevel)*100))
case UIDevice.batteryStateDidChangeNotification:
message = ORIOSPerformanceEvent(name: "batteryState", value: UInt64(UIDevice.current.batteryState.rawValue))
case UIDevice.orientationDidChangeNotification:
message = ORIOSPerformanceEvent(name: "orientation", value: UInt64(UIDevice.current.orientation.rawValue))
default: break
}
if let message = message {
MessageCollector.shared.sendMessage(message)
}
}
func networkStateChange(_ state: UInt64) {
let message = ORIOSPerformanceEvent(name: "networkState", value: state)
MessageCollector.shared.sendMessage(message)
}
func memoryUsage() -> UInt64? {
var taskInfo = task_vm_info_data_t()
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info>.size) / 4
let result: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
guard result == KERN_SUCCESS else {
return nil
}
return UInt64(taskInfo.phys_footprint)
}
func cpuUsage() -> Double? {
var threadsListContainer: thread_act_array_t?
var threadsCount = mach_msg_type_number_t(0)
let threadsResult = withUnsafeMutablePointer(to: &threadsListContainer) {
return $0.withMemoryRebound(to: thread_act_array_t?.self, capacity: 1) {
task_threads(mach_task_self_, $0, &threadsCount)
}
}
defer {
vm_deallocate(mach_task_self_, vm_address_t(UInt(bitPattern: threadsListContainer)), vm_size_t(Int(threadsCount) * MemoryLayout<thread_t>.stride))
}
guard threadsCount > 0, threadsResult == KERN_SUCCESS, let threadsList = threadsListContainer else {
return nil
}
var threadInfo = thread_basic_info()
var threadInfoCount = mach_msg_type_number_t(THREAD_INFO_MAX)
let infoResult = withUnsafeMutablePointer(to: &threadInfo) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
thread_info(threadsList[0], thread_flavor_t(THREAD_BASIC_INFO), $0, &threadInfoCount)
}
}
let threadBasicInfo = threadInfo as thread_basic_info
guard infoResult == KERN_SUCCESS, threadBasicInfo.flags & TH_FLAGS_IDLE == 0 else { return nil }
return Double(threadBasicInfo.cpu_usage) / Double(TH_USAGE_SCALE) * 100.0
}
}

View file

@ -1,16 +0,0 @@
import Foundation
class DebugUtils: NSObject {
static func error(_ str: String) {
// TODO: fix this one
// MessageCollector.shared.sendMessage(ASIOSInternalError(content: str))
log(str)
}
static func log(_ str: String) {
#if DEBUG
print(str)
#endif
}
}

View file

@ -1,168 +0,0 @@
import UIKit
struct BatchArch {
var name: String
var data: Data
}
class MessageCollector: NSObject {
public static let shared = MessageCollector()
private var imagesWaiting = [BatchArch]()
private var imagesSending = [BatchArch]()
private var messagesWaiting = [Data]()
private var nextMessageIndex = 0
private var sendingLastMessages = false
private let maxMessagesSize = 500_000
private let messagesQueue = OperationQueue()
private let lateMessagesFile: URL?
private var sendInderval: Timer?
override init() {
lateMessagesFile = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("/lateMessages.dat")
super.init()
}
func start() {
sendInderval = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [weak self] _ in
self?.flush()
})
NotificationCenter.default.addObserver(self, selector: #selector(terminate), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(terminate), name: UIApplication.willTerminateNotification, object: nil)
messagesQueue.maxConcurrentOperationCount = 1
if let fileUrl = lateMessagesFile,
FileManager.default.fileExists(atPath: fileUrl.path),
let lateData = try? Data(contentsOf: fileUrl) {
NetworkManager.shared.sendLateMessage(content: lateData) { (success) in
guard success else { return }
try? FileManager.default.removeItem(at: fileUrl)
}
}
}
func stop() {
sendInderval?.invalidate()
NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.willTerminateNotification, object: nil)
self.terminate()
}
func sendImagesBatch(batch: Data, fileName: String) {
self.imagesWaiting.append(BatchArch(name: fileName, data: batch))
messagesQueue.addOperation {
self.flushImages()
}
}
@objc func terminate() {
guard !sendingLastMessages else { return }
messagesQueue.addOperation {
self.sendingLastMessages = true
self.flushMessages()
}
}
@objc func flush() {
messagesQueue.addOperation {
self.flushMessages()
self.flushImages()
}
}
private func flushImages() {
let images = imagesWaiting.first
guard !imagesWaiting.isEmpty, let images = images, let projectKey = ORTracker.shared.projectKey else { return }
imagesWaiting.remove(at: 0)
imagesSending.append(images)
DebugUtils.log("Sending images \(images.name) \(images.data.count)")
NetworkManager.shared.sendImages(projectKey: projectKey, images: images.data, name: images.name) { (success) in
self.imagesSending.removeAll { (waiting) -> Bool in
images.name == waiting.name
}
guard success else {
self.imagesWaiting.append(images)
return
}
}
}
func sendMessage(_ message: ORMessage) {
let data = message.contentData()
#if DEBUG
if !message.description.contains("IOSLog") && !message.description.contains("IOSNetworkCall") {
DebugUtils.log(message.description)
}
if let networkCallMessage = message as? ORIOSNetworkCall {
DebugUtils.log("-->> IOSNetworkCall(105): \(networkCallMessage.method) \(networkCallMessage.URL)")
}
#endif
self.sendRawMessage(data)
}
private var debounceTimer: Timer?
private var debouncedMessage: ORMessage?
func sendDebouncedMessage(_ message: ORMessage) {
debounceTimer?.invalidate()
debouncedMessage = message
debounceTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in
if let debouncedMessage = self?.debouncedMessage {
self?.sendMessage(debouncedMessage)
self?.debouncedMessage = nil
}
}
}
func sendRawMessage(_ data: Data) {
messagesQueue.addOperation {
if data.count > self.maxMessagesSize {
DebugUtils.log("<><><>Single message size exceeded limit")
return
}
self.messagesWaiting.append(data)
var totalWaitingSize = 0
self.messagesWaiting.forEach { totalWaitingSize += $0.count }
if totalWaitingSize > Int(Double(self.maxMessagesSize) * 0.8) {
self.flushMessages()
}
}
}
private func flushMessages() {
var messages = [Data]()
var sentSize = 0
while let message = messagesWaiting.first, sentSize + message.count <= maxMessagesSize {
messages.append(message)
messagesWaiting.remove(at: 0)
sentSize += message.count
}
guard !messages.isEmpty else { return }
var content = Data()
let index = ORIOSBatchMeta(firstIndex: UInt64(nextMessageIndex))
content.append(index.contentData())
DebugUtils.log(index.description)
messages.forEach { (message) in
content.append(message)
}
if sendingLastMessages, let fileUrl = lateMessagesFile {
try? content.write(to: fileUrl)
}
nextMessageIndex += messages.count
DebugUtils.log("messages batch \(content)")
NetworkManager.shared.sendMessage(content: content) { (success) in
guard success else {
DebugUtils.log("<><>re-sending failed batch<><>")
self.messagesWaiting.insert(contentsOf: messages, at: 0)
return
}
if self.sendingLastMessages {
self.sendingLastMessages = false
if let fileUrl = self.lateMessagesFile, FileManager.default.fileExists(atPath: fileUrl.path) {
try? FileManager.default.removeItem(at: fileUrl)
}
}
}
}
}

View file

@ -1,195 +0,0 @@
import UIKit
import SWCompression
let START_URL = "/v1/mobile/start"
let INGEST_URL = "/v1/mobile/i"
let LATE_URL = "/v1/mobile/late"
let IMAGES_URL = "/v1/mobile/images"
class NetworkManager: NSObject {
static let shared = NetworkManager()
var baseUrl = "https://api.openreplay.com/ingest"
public var sessionId: String? = nil
private var token: String? = nil
public var writeToFile = false
#if DEBUG
private let localFilePath = "/Users/nikitamelnikov/Desktop/session.dat"
#endif
override init() {
#if DEBUG
if writeToFile, FileManager.default.fileExists(atPath: localFilePath) {
try? FileManager.default.removeItem(at: URL(fileURLWithPath: localFilePath))
}
#endif
}
private func createRequest(method: String, path: String) -> URLRequest {
let url = URL(string: baseUrl+path)!
var request = URLRequest(url: url)
request.httpMethod = method
return request
}
private func callAPI(request: URLRequest,
onSuccess: @escaping (Data) -> Void,
onError: @escaping (Error?) -> Void) {
guard !writeToFile else { return }
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
DebugUtils.log(">>>\(request.httpMethod ?? ""):\(request.url?.absoluteString ?? "")\n<<<\(String(data: data ?? Data(), encoding: .utf8) ?? "")")
DispatchQueue.main.async {
guard let data = data,
let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
DebugUtils.error(">>>>>> Error in call \(request.url?.absoluteString ?? "") : \(error?.localizedDescription ?? "N/A")")
if (response as? HTTPURLResponse)?.statusCode == 401 {
self.token = nil
ORTracker.shared.startSession(projectKey: ORTracker.shared.projectKey ?? "", options: ORTracker.shared.options)
}
onError(error)
return
}
onSuccess(data)
}
}
task.resume()
}
func createSession(params: [String: AnyHashable], completion: @escaping (ORSessionResponse?) -> Void) {
guard !writeToFile else {
self.token = "writeToFile"
return
}
var request = createRequest(method: "POST", path: START_URL)
guard let jsonData = try? JSONSerialization.data(withJSONObject: params, options: []) else {
completion(nil)
return
}
request.httpBody = jsonData
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
callAPI(request: request) { (data) in
do {
let session = try JSONDecoder().decode(ORSessionResponse.self, from: data)
self.token = session.token
self.sessionId = session.sessionID
ORUserDefaults.shared.lastToken = self.token
completion(session)
} catch {
DebugUtils.log("Can't unwrap session start resp: \(error)")
}
} onError: { _ in
completion(nil)
}
}
func sendMessage(content: Data, completion: @escaping (Bool) -> Void) {
guard !writeToFile else {
appendLocalFile(data: content)
return
}
var request = createRequest(method: "POST", path: INGEST_URL)
guard let token = token else {
completion(false)
return
}
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
var compressedContent = content
let oldSize = compressedContent.count
var newSize = oldSize
do {
let compressed = try GzipArchive.archive(data: content)
compressedContent = compressed
newSize = compressed.count
request.setValue("gzip", forHTTPHeaderField: "Content-Encoding")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
DebugUtils.log(">>>>Compress batch file \(oldSize)>\(newSize)")
} catch {
DebugUtils.log("Error with compression: \(error)")
}
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.httpBody = compressedContent
callAPI(request: request) { (data) in
completion(true)
} onError: { _ in
completion(false)
}
}
func sendLateMessage(content: Data, completion: @escaping (Bool) -> Void) {
DebugUtils.log(">>>sending late messages")
var request = createRequest(method: "POST", path: LATE_URL)
guard let token = ORUserDefaults.shared.lastToken else {
completion(false)
DebugUtils.log("! No last token found")
return
}
print(token)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.httpBody = content
callAPI(request: request) { (data) in
completion(true)
DebugUtils.log("<<< late messages sent")
} onError: { _ in
completion(false)
}
}
func sendImages(projectKey: String, images: Data, name: String, completion: @escaping (Bool) -> Void) {
var request = createRequest(method: "POST", path: IMAGES_URL)
guard let token = token else {
completion(false)
return
}
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let boundary = "Boundary-\(NSUUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
var body = Data()
let parameters = ["projectKey": projectKey]
for (key, value) in parameters {
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
body.appendString("\(value)\r\n")
}
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"batch\"; filename=\"\(name)\"\r\n")
body.appendString("Content-Type: gzip\r\n\r\n")
body.append(images)
body.appendString("\r\n")
body.appendString("--\(boundary)--\r\n")
DebugUtils.log(">>>>>> sending \(body.count) bytes")
request.httpBody = body
callAPI(request: request) { (data) in
completion(true)
} onError: { _ in
completion(false)
}
}
private func appendLocalFile(data: Data) {
#if DEBUG
DebugUtils.log("appendInFile \(data.count) bytes")
let fileURL = URL(fileURLWithPath: localFilePath)
if let fileHandle = try? FileHandle(forWritingTo: fileURL) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(data)
} else {
try? data.write(to: fileURL, options: .atomic)
}
#endif
}
}

View file

@ -1,33 +0,0 @@
import UIKit
class ORUserDefaults: NSObject {
public static let shared = ORUserDefaults()
private let userDefaults: UserDefaults?
override init() {
userDefaults = UserDefaults(suiteName: "io.orenreplay.openreplaytr-defaults")
}
var userUUID: String {
get {
if let savedUUID = userDefaults?.string(forKey: "userUUID") {
return savedUUID
}
let newUUID = UUID().uuidString
self.userUUID = newUUID
return newUUID
}
set {
userDefaults?.set(newValue, forKey: "userUUID")
}
}
var lastToken: String? {
get {
return userDefaults?.string(forKey: "lastToken")
}
set {
userDefaults?.set(newValue, forKey: "lastToken")
}
}
}

View file

@ -1,22 +0,0 @@
import UIKit
class Swizzling: NSObject {
static func swizzle(cls: AnyClass, original: Selector, swizzled: Selector) {
if let originalMethod = class_getInstanceMethod(cls, original),
let swizzledMethod = class_getInstanceMethod(cls, swizzled) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
static func swizzleIfPresent(cls: AnyClass, original: Selector, swizzled: Selector) {
if let originalMethod = class_getInstanceMethod(cls, original),
let swizzledMethod = class_getInstanceMethod(cls, swizzled) {
let didAddMethod = class_addMethod(self, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
}

View file

@ -1,42 +0,0 @@
import UIKit
struct GenericMessage {
let typeRaw: UInt64
let type: ORMessageType?
let timestamp: UInt64
let body: Data
init?(data: Data, offset: inout Int) {
do {
typeRaw = try data.readPrimary(offset: &offset)
type = ORMessageType(rawValue: typeRaw)
timestamp = try data.readPrimary(offset: &offset)
body = try data.readData(offset: &offset)
} catch {
return nil
}
}
}
class ORMessage: NSObject {
let messageRaw: UInt64
let message: ORMessageType?
let timestamp: UInt64
init(messageType: ORMessageType) {
self.messageRaw = messageType.rawValue
self.message = messageType
self.timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
}
init?(genericMessage: GenericMessage) {
self.messageRaw = genericMessage.typeRaw
self.message = genericMessage.type
self.timestamp = genericMessage.timestamp
}
func contentData() -> Data {
fatalError("This method should be overridden")
}
}

View file

@ -1,27 +0,0 @@
import Foundation
@objc public enum RecordingQuality: Int {
case Low
case Standard
case High
}
open class OROptions: NSObject {
let crashes: Bool
let analytics: Bool
let performances: Bool
let logs: Bool
let screen: Bool
let wifiOnly: Bool
public static let defaults = OROptions(crashes: true, analytics: true, performances: true, logs: true, screen: true, wifiOnly: true)
@objc public init(crashes: Bool, analytics: Bool, performances: Bool, logs: Bool, screen: Bool, wifiOnly: Bool) {
self.crashes = crashes
self.analytics = analytics
self.performances = performances
self.logs = logs
self.screen = screen
self.wifiOnly = wifiOnly
}
}

View file

@ -1,9 +0,0 @@
import UIKit
struct ORRecord: Codable {
let img: String
let originX: CGFloat
let originY: CGFloat
let timestamp: UInt64
}

View file

@ -1,84 +0,0 @@
import UIKit
import DeviceKit
class ORSessionRequest: NSObject {
private static var params = [String: AnyHashable]()
static func create( completion: @escaping (ORSessionResponse?) -> Void) {
guard let projectKey = ORTracker.shared.projectKey else { return print("Openreplay: no project key added") }
#warning("Can interfere with client usage")
UIDevice.current.isBatteryMonitoringEnabled = true
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
let performances: [String: UInt64] = [
"physicalMemory": UInt64(ProcessInfo.processInfo.physicalMemory),
"processorCount": UInt64(ProcessInfo.processInfo.processorCount),
"activeProcessorCount": UInt64(ProcessInfo.processInfo.activeProcessorCount),
"systemUptime": UInt64(ProcessInfo.processInfo.systemUptime),
"isLowPowerModeEnabled": UInt64(ProcessInfo.processInfo.isLowPowerModeEnabled ? 1 : 0),
"thermalState": UInt64(ProcessInfo.processInfo.thermalState.rawValue),
"batteryLevel": UInt64(max(0.0, UIDevice.current.batteryLevel)*100),
"batteryState": UInt64(UIDevice.current.batteryState.rawValue),
"orientation": UInt64(UIDevice.current.orientation.rawValue),
]
let device = Device.current
var deviceModel = ""
var deviceSafeName = ""
if device.isSimulator {
deviceSafeName = "iPhone 14 Pro"
deviceModel = "iPhone14,8"
} else {
deviceSafeName = device.safeDescription
deviceModel = Device.identifier
}
DebugUtils.log(">>>> device \(device) type \(device.safeDescription) mem \(UInt64(ProcessInfo.processInfo.physicalMemory / 1024))")
params = [
"projectKey": projectKey,
"trackerVersion": Bundle(for: ORTracker.shared.classForCoder).object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "N/A",
"revID": Bundle(for: ORTracker.shared.classForCoder).object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "N/A",
"userUUID": ORUserDefaults.shared.userUUID,
"userOSVersion": UIDevice.current.systemVersion,
"userDevice": deviceModel,
"userDeviceType": deviceSafeName,
"timestamp": UInt64(Date().timeIntervalSince1970 * 1000),
"performances": performances,
"deviceMemory": UInt64(ProcessInfo.processInfo.physicalMemory / 1024),
"timezone": getTimezone(),
]
callAPI(completion: completion)
}
private static func callAPI(completion: @escaping (ORSessionResponse?) -> Void) {
guard !params.isEmpty else { return }
NetworkManager.shared.createSession(params: params) { (sessionResponse) in
guard let sessionResponse = sessionResponse else {
DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
callAPI(completion: completion)
}
return
}
DebugUtils.log(">>>> Starting session : \(sessionResponse.sessionID)")
return completion(sessionResponse)
}
}
}
struct ORSessionResponse: Decodable {
let userUUID: String
let token: String
let imagesHashList: [String]?
let sessionID: String
let fps: Int
let quality: String
}
func getTimezone() -> String {
let offset = TimeZone.current.secondsFromGMT()
let sign = offset >= 0 ? "+" : "-"
let hours = abs(offset) / 3600
let minutes = (abs(offset) % 3600) / 60
return String(format: "UTC%@%02d:%02d", sign, hours, minutes)
}

View file

@ -1,499 +0,0 @@
// Auto-generated, do not edit
import UIKit
enum ORMessageType: UInt64 {
case iOSMetadata = 92
case iOSEvent = 93
case iOSUserID = 94
case iOSUserAnonymousID = 95
case iOSScreenChanges = 96
case iOSCrash = 97
case iOSViewComponentEvent = 98
case iOSClickEvent = 100
case iOSInputEvent = 101
case iOSPerformanceEvent = 102
case iOSLog = 103
case iOSInternalError = 104
case iOSNetworkCall = 105
case iOSSwipeEvent = 106
case iOSBatchMeta = 107
}
class ORIOSMetadata: ORMessage {
let key: String
let value: String
init(key: String, value: String) {
self.key = key
self.value = value
super.init(messageType: .iOSMetadata)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.key = try genericMessage.body.readString(offset: &offset)
self.value = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(92), timestamp, Data(values: key, value))
}
override var description: String {
return "-->> IOSMetadata(92): timestamp:\(timestamp) key:\(key) value:\(value)";
}
}
class ORIOSEvent: ORMessage {
let name: String
let payload: String
init(name: String, payload: String) {
self.name = name
self.payload = payload
super.init(messageType: .iOSEvent)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.name = try genericMessage.body.readString(offset: &offset)
self.payload = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(93), timestamp, Data(values: name, payload))
}
override var description: String {
return "-->> IOSEvent(93): timestamp:\(timestamp) name:\(name) payload:\(payload)";
}
}
class ORIOSUserID: ORMessage {
let iD: String
init(iD: String) {
self.iD = iD
super.init(messageType: .iOSUserID)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.iD = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(94), timestamp, Data(values: iD))
}
override var description: String {
return "-->> IOSUserID(94): timestamp:\(timestamp) iD:\(iD)";
}
}
class ORIOSUserAnonymousID: ORMessage {
let iD: String
init(iD: String) {
self.iD = iD
super.init(messageType: .iOSUserAnonymousID)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.iD = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(95), timestamp, Data(values: iD))
}
override var description: String {
return "-->> IOSUserAnonymousID(95): timestamp:\(timestamp) iD:\(iD)";
}
}
class ORIOSScreenChanges: ORMessage {
let x: UInt64
let y: UInt64
let width: UInt64
let height: UInt64
init(x: UInt64, y: UInt64, width: UInt64, height: UInt64) {
self.x = x
self.y = y
self.width = width
self.height = height
super.init(messageType: .iOSScreenChanges)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.x = try genericMessage.body.readPrimary(offset: &offset)
self.y = try genericMessage.body.readPrimary(offset: &offset)
self.width = try genericMessage.body.readPrimary(offset: &offset)
self.height = try genericMessage.body.readPrimary(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(96), timestamp, Data(values: x, y, width, height))
}
override var description: String {
return "-->> IOSScreenChanges(96): timestamp:\(timestamp) x:\(x) y:\(y) width:\(width) height:\(height)";
}
}
class ORIOSCrash: ORMessage {
let name: String
let reason: String
let stacktrace: String
init(name: String, reason: String, stacktrace: String) {
self.name = name
self.reason = reason
self.stacktrace = stacktrace
super.init(messageType: .iOSCrash)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.name = try genericMessage.body.readString(offset: &offset)
self.reason = try genericMessage.body.readString(offset: &offset)
self.stacktrace = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(97), timestamp, Data(values: name, reason, stacktrace))
}
override var description: String {
return "-->> IOSCrash(97): timestamp:\(timestamp) name:\(name) reason:\(reason) stacktrace:\(stacktrace)";
}
}
class ORIOSViewComponentEvent: ORMessage {
let screenName: String
let viewName: String
let visible: Bool
init(screenName: String, viewName: String, visible: Bool) {
self.screenName = screenName
self.viewName = viewName
self.visible = visible
super.init(messageType: .iOSViewComponentEvent)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.screenName = try genericMessage.body.readString(offset: &offset)
self.viewName = try genericMessage.body.readString(offset: &offset)
self.visible = try genericMessage.body.readPrimary(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(98), timestamp, Data(values: screenName, viewName, visible))
}
override var description: String {
return "-->> IOSViewComponentEvent(98): timestamp:\(timestamp) screenName:\(screenName) viewName:\(viewName) visible:\(visible)";
}
}
class ORIOSClickEvent: ORMessage {
let label: String
let x: UInt64
let y: UInt64
init(label: String, x: UInt64, y: UInt64) {
self.label = label
self.x = x
self.y = y
super.init(messageType: .iOSClickEvent)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.label = try genericMessage.body.readString(offset: &offset)
self.x = try genericMessage.body.readPrimary(offset: &offset)
self.y = try genericMessage.body.readPrimary(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(100), timestamp, Data(values: label, x, y))
}
override var description: String {
return "-->> IOSClickEvent(100): timestamp:\(timestamp) label:\(label) x:\(x) y:\(y)";
}
}
class ORIOSInputEvent: ORMessage {
let value: String
let valueMasked: Bool
let label: String
init(value: String, valueMasked: Bool, label: String) {
self.value = value
self.valueMasked = valueMasked
self.label = label
super.init(messageType: .iOSInputEvent)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.value = try genericMessage.body.readString(offset: &offset)
self.valueMasked = try genericMessage.body.readPrimary(offset: &offset)
self.label = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(101), timestamp, Data(values: value, valueMasked, label))
}
override var description: String {
return "-->> IOSInputEvent(101): timestamp:\(timestamp) value:\(value) valueMasked:\(valueMasked) label:\(label)";
}
}
class ORIOSPerformanceEvent: ORMessage {
let name: String
let value: UInt64
init(name: String, value: UInt64) {
self.name = name
self.value = value
super.init(messageType: .iOSPerformanceEvent)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.name = try genericMessage.body.readString(offset: &offset)
self.value = try genericMessage.body.readPrimary(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(102), timestamp, Data(values: name, value))
}
override var description: String {
return "-->> IOSPerformanceEvent(102): timestamp:\(timestamp) name:\(name) value:\(value)";
}
}
class ORIOSLog: ORMessage {
let severity: String
let content: String
init(severity: String, content: String) {
self.severity = severity
self.content = content
super.init(messageType: .iOSLog)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.severity = try genericMessage.body.readString(offset: &offset)
self.content = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(103), timestamp, Data(values: severity, content))
}
override var description: String {
return "-->> IOSLog(103): timestamp:\(timestamp) severity:\(severity) content:\(content)";
}
}
class ORIOSInternalError: ORMessage {
let content: String
init(content: String) {
self.content = content
super.init(messageType: .iOSInternalError)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.content = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(104), timestamp, Data(values: content))
}
override var description: String {
return "-->> IOSInternalError(104): timestamp:\(timestamp) content:\(content)";
}
}
class ORIOSNetworkCall: ORMessage {
let type: String
let method: String
let URL: String
let request: String
let response: String
let status: UInt64
let duration: UInt64
init(type: String, method: String, URL: String, request: String, response: String, status: UInt64, duration: UInt64) {
self.type = type
self.method = method
self.URL = URL
self.request = request
self.response = response
self.status = status
self.duration = duration
super.init(messageType: .iOSNetworkCall)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.type = try genericMessage.body.readString(offset: &offset)
self.method = try genericMessage.body.readString(offset: &offset)
self.URL = try genericMessage.body.readString(offset: &offset)
self.request = try genericMessage.body.readString(offset: &offset)
self.response = try genericMessage.body.readString(offset: &offset)
self.status = try genericMessage.body.readPrimary(offset: &offset)
self.duration = try genericMessage.body.readPrimary(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(105), timestamp, Data(values: type, method, URL, request, response, status, duration))
}
override var description: String {
return "-->> IOSNetworkCall(105): timestamp:\(timestamp) type:\(type) method:\(method) URL:\(URL) request:\(request) response:\(response) status:\(status) duration:\(duration)";
}
}
class ORIOSSwipeEvent: ORMessage {
let label: String
let x: UInt64
let y: UInt64
let direction: String
init(label: String, x: UInt64, y: UInt64, direction: String) {
self.label = label
self.x = x
self.y = y
self.direction = direction
super.init(messageType: .iOSSwipeEvent)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.label = try genericMessage.body.readString(offset: &offset)
self.x = try genericMessage.body.readPrimary(offset: &offset)
self.y = try genericMessage.body.readPrimary(offset: &offset)
self.direction = try genericMessage.body.readString(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(106), timestamp, Data(values: label, x, y, direction))
}
override var description: String {
return "-->> IOSSwipeEvent(106): timestamp:\(timestamp) label:\(label) x:\(x) y:\(y) direction:\(direction)";
}
}
class ORIOSBatchMeta: ORMessage {
let firstIndex: UInt64
init(firstIndex: UInt64) {
self.firstIndex = firstIndex
super.init(messageType: .iOSBatchMeta)
}
override init?(genericMessage: GenericMessage) {
do {
var offset = 0
self.firstIndex = try genericMessage.body.readPrimary(offset: &offset)
super.init(genericMessage: genericMessage)
} catch {
return nil
}
}
override func contentData() -> Data {
return Data(values: UInt64(107), timestamp, Data(values: firstIndex))
}
override var description: String {
return "-->> IOSBatchMeta(107): timestamp:\(timestamp) firstIndex:\(firstIndex)";
}
}

View file

@ -1,37 +0,0 @@
// Auto-generated, do not edit
import UIKit
enum ORMessageType: UInt64 {
<%= $messages.map { |msg| " case #{msg.name.first_lower} = #{msg.id}" }.join "\n" %>
}
<% $messages.each do |msg| %>
class OR<%= msg.name.to_s.camel_case %>: ORMessage {
<%= msg.attributes[2..-1].map { |attr| " let #{attr.property}: #{attr.type_swift}" }.join "\n" %>
init(<%= msg.attributes[2..-1].map { |attr| "#{attr.property}: #{attr.type_swift}" }.join ", " %>) {
<%= msg.attributes[2..-1].map { |attr| " self.#{attr.property} = #{attr.property}" }.join "\n" %>
super.init(messageType: .<%= "#{msg.name.first_lower}" %>)
}
override init?(genericMessage: GenericMessage) {
<% if msg.attributes.length > 2 %> do {
var offset = 0
<%= msg.attributes[2..-1].map { |attr| " self.#{attr.property} = try genericMessage.body.read#{attr.type_swift_read}(offset: &offset)" }.join "\n" %>
super.init(genericMessage: genericMessage)
} catch {
return nil
}
<% else %>
super.init(genericMessage: genericMessage)
<% end %>}
override func contentData() -> Data {
return Data(values: UInt64(<%= "#{msg.id}"%>), timestamp<% if msg.attributes.length > 2 %>, Data(values: <%= msg.attributes[2..-1].map { |attr| attr.property }.join ", "%>)<% end %>)
}
override var description: String {
return "-->> <%= msg.name.to_s.camel_case %>(<%= "#{msg.id}"%>): timestamp:\(timestamp) <%= msg.attributes[2..-1].map { |attr| "#{attr.property}:\\(#{attr.property})" }.join " "%>";
}
}
<% end %>

View file

@ -1,129 +0,0 @@
message 92, 'IOSMetadata' do
uint 'Timestamp'
uint 'Length'
string 'Key'
string 'Value'
end
message 93, 'IOSEvent' do
uint 'Timestamp'
uint 'Length'
string 'Name'
string 'Payload'
end
message 94, 'IOSUserID' do
uint 'Timestamp'
uint 'Length'
string 'ID'
end
message 95, 'IOSUserAnonymousID' do
uint 'Timestamp'
uint 'Length'
string 'ID'
end
message 96, 'IOSScreenChanges' do
uint 'Timestamp'
uint 'Length'
uint 'X'
uint 'Y'
uint 'Width'
uint 'Height'
end
message 97, 'IOSCrash' do
uint 'Timestamp'
uint 'Length'
string 'Name'
string 'Reason'
string 'Stacktrace'
end
message 98, 'IOSViewComponentEvent' do
uint 'Timestamp'
uint 'Length'
string 'ScreenName'
string 'ViewName'
boolean 'Visible'
end
message 100, 'IOSClickEvent' do
uint 'Timestamp'
uint 'Length'
string 'Label'
uint 'X'
uint 'Y'
end
message 101, 'IOSInputEvent' do
uint 'Timestamp'
uint 'Length'
string 'Value'
boolean 'ValueMasked'
string 'Label'
end
=begin
Name/Value may be :
"physicalMemory": Total memory in bytes
"processorCount": Total processors in device
"activeProcessorCount": Number of currently used processors
"systemUptime": Elapsed time (in seconds) since last boot
"isLowPowerModeEnabled": Possible values (1 or 0)
"thermalState": Possible values (0:nominal 1:fair 2:serious 3:critical)
"batteryLevel": Possible values (0 .. 100)
"batteryState": Possible values (0:unknown 1:unplugged 2:charging 3:full)
"orientation": Possible values (0unknown 1:portrait 2:portraitUpsideDown 3:landscapeLeft 4:landscapeRight 5:faceUp 6:faceDown)
"mainThreadCPU": Possible values (0 .. 100)
"memoryUsage": Used memory in bytes
"fps": Frames per second
=end
message 102, 'IOSPerformanceEvent' do
uint 'Timestamp'
uint 'Length'
string 'Name'
uint 'Value'
end
message 103, 'IOSLog' do
uint 'Timestamp'
uint 'Length'
string 'Severity' # Possible values ("info", "error")
string 'Content'
end
message 104, 'IOSInternalError' do
uint 'Timestamp'
uint 'Length'
string 'Content'
end
message 105, 'IOSNetworkCall' do
uint 'Timestamp'
uint 'Length'
string 'Type'
string 'Method'
string 'URL'
string 'Request'
string 'Response'
uint 'Status'
uint 'Duration'
end
message 106, 'IOSSwipeEvent' do
uint 'Timestamp'
uint 'Length'
string 'Label'
uint 'X'
uint 'Y'
string 'Direction'
end
message 107, 'IOSBatchMeta' do
uint 'Timestamp'
uint 'Length'
uint 'FirstIndex'
end

View file

@ -1,114 +0,0 @@
require 'erb'
class String
def camel_case
return self if self !~ /_/ && self =~ /[A-Z]+.*/
split('_').map{|e| e.capitalize}.join.upperize
end
def camel_case_lower
self.split('_').inject([]) do |buffer, e|
word = if e.downcase == "url"
"URL"
else
buffer.empty? ? e : e.capitalize
end
buffer.push(word)
end.join.upperize
end
def upperize
self.sub('Id', 'ID').sub('Url', 'URL').sub('url', 'URL')
end
def first_lower
return "URL" if self == "URL"
"" if self == ""
self[0].downcase + self[1..-1]
end
def underscore
self.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
end
end
class Attribute
attr_reader :name, :type
def initialize(name:, type:)
@name = name
@type = type
end
def property
@name.first_lower
end
def type_swift
case @type
when :string
"String"
when :data
"Data"
when :uint
"UInt64"
when :boolean
"Bool"
else
"Primary"
end
end
def type_swift_read
case @type
when :string
"String"
when :data
"Data"
else
"Primary"
end
end
end
class Message
attr_reader :id, :name, :js, :replayer, :attributes
def initialize(name:, id:, js: true, replayer: true, &block)
@id = id
@name = name
@js = js
@replayer = replayer
@attributes = []
instance_eval &block
end
%i(uint string data boolean).each do |type|
define_method type do |name, opts = {}|
opts.merge!(
name: name,
type: type,
)
@attributes << Attribute.new(opts)
end
end
end
$ids = []
$messages = []
def message(id, name, opts = {}, &block)
raise "id duplicated #{name}" if $ids.include? id
raise "id is too big #{name}" if id > 120
$ids << id
opts[:id] = id
opts[:name] = name
msg = Message.new(opts, &block)
$messages << msg
end
require './messages.rb'
e = ERB.new(File.read('./messages.erb'))
File.write('ORMessages.swift', e.result)

View file

@ -1,162 +0,0 @@
import UIKit
import Network
public enum CheckState {
case unchecked
case canStart
case cantStart
}
open class ORTracker: NSObject {
@objc public static let shared = ORTracker()
public let userDefaults = UserDefaults(suiteName: "io.asayer.AsayerSDK-defaults")
public var projectKey: String?
public var trackerState = CheckState.unchecked
private var networkCheckTimer: Timer?
public var serverURL: String {
get { NetworkManager.shared.baseUrl }
set { NetworkManager.shared.baseUrl = newValue }
}
public var options: OROptions = OROptions.defaults
@objc open func start(projectKey: String, options: OROptions) {
self.options = options
let monitor = NWPathMonitor()
let q = DispatchQueue.global(qos: .background)
self.projectKey = projectKey
monitor.start(queue: q)
monitor.pathUpdateHandler = { path in
if path.usesInterfaceType(.wifi) {
if PerformanceListener.shared.isActive {
PerformanceListener.shared.networkStateChange(1)
}
self.trackerState = CheckState.canStart
} else if path.usesInterfaceType(.cellular) {
if PerformanceListener.shared.isActive {
PerformanceListener.shared.networkStateChange(0)
}
if options.wifiOnly {
self.trackerState = CheckState.cantStart
print("Connected to Cellular and options.wifiOnly is true. ORTracker will not start.")
} else {
self.trackerState = CheckState.canStart
}
} else {
self.trackerState = CheckState.cantStart
print("Not connected to either WiFi or Cellular. ORTracker will not start.")
}
}
networkCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (_) in
if self.trackerState == CheckState.canStart {
self.startSession(projectKey: projectKey, options: options)
self.networkCheckTimer?.invalidate()
}
if self.trackerState == CheckState.cantStart {
self.networkCheckTimer?.invalidate()
}
})
}
@objc open func startSession(projectKey: String, options: OROptions) {
self.projectKey = projectKey
ORSessionRequest.create() { sessionResponse in
guard let sessionResponse = sessionResponse else { return print("Openreplay: no response from /start request") }
let captureSettings = getCaptureSettings(fps: sessionResponse.fps, quality: sessionResponse.quality)
ScreenshotManager.shared.setSettings(settings: captureSettings)
MessageCollector.shared.start()
if options.logs {
LogsListener.shared.start()
}
if options.crashes {
Crashs.shared.start()
}
if options.performances {
PerformanceListener.shared.start()
}
if options.screen {
ScreenshotManager.shared.start()
}
if options.analytics {
Analytics.shared.start()
}
}
}
@objc open func stop() {
MessageCollector.shared.stop()
ScreenshotManager.shared.stop()
Crashs.shared.stop()
PerformanceListener.shared.stop()
Analytics.shared.stop()
}
@objc open func addIgnoredView(_ view: UIView) {
ScreenshotManager.shared.addSanitizedElement(view)
}
@objc open func setMetadata(key: String, value: String) {
let message = ORIOSMetadata(key: key, value: value)
MessageCollector.shared.sendMessage(message)
}
@objc open func event(name: String, object: NSObject?) {
event(name: name, payload: object as? Encodable)
}
open func event(name: String, payload: Encodable?) {
var json = ""
if let payload = payload,
let data = payload.toJSONData(),
let jsonStr = String(data: data, encoding: .utf8) {
json = jsonStr
}
let message = ORIOSEvent(name: name, payload: json)
MessageCollector.shared.sendMessage(message)
}
open func eventStr(name: String, payload: String?) {
let message = ORIOSEvent(name: name, payload: payload ?? "")
MessageCollector.shared.sendMessage(message)
}
@objc open func setUserID(_ userID: String) {
let message = ORIOSUserID(iD: userID)
MessageCollector.shared.sendMessage(message)
}
@objc open func userAnonymousID(_ userID: String) {
let message = ORIOSUserAnonymousID(iD: userID)
MessageCollector.shared.sendMessage(message)
}
}
func getCaptureSettings(fps: Int, quality: String) -> (captureRate: Double, imgCompression: Double) {
let limitedFPS = min(max(fps, 1), 99)
let captureRate = 1.0 / Double(limitedFPS)
var imgCompression: Double
switch quality.lowercased() {
case "low":
imgCompression = 0.4
case "standard":
imgCompression = 0.5
case "high":
imgCompression = 0.6
default:
imgCompression = 0.5 // default to standard if quality string is not recognized
}
return (captureRate: captureRate, imgCompression: imgCompression)
}

View file

@ -1,286 +0,0 @@
import UIKit
import Foundation
import SwiftUI
import SWCompression
// MARK: - screenshot manager
open class ScreenshotManager {
public static let shared = ScreenshotManager()
private let messagesQueue = OperationQueue()
private var timer: Timer?
private var sendTimer: Timer?
private var sanitizedElements: [Sanitizable] = []
private var observedInputs: [UITextField] = []
private var screenshots: [Data] = []
private var lastIndex = 0
// MARK: capture settings
// should we blur out sensitive views, or place a solid box on top
private var isBlurMode = true
private var blurRadius = 2.5
// this affects how big the image will be compared to real phone screan.
// we also can use default UIScreen.main.scale which is around 3.0 (dense pixel screen)
private var screenScale = 1.25
private var settings: (captureRate: Double, imgCompression: Double) = (captureRate: 0.33, imgCompression: 0.5)
private init() { }
func start() {
startTakingScreenshots(every: settings.captureRate)
}
func setSettings(settings: (captureRate: Double, imgCompression: Double)) {
self.settings = settings
}
func stop() {
timer?.invalidate()
timer = nil
lastIndex = 0
screenshots.removeAll()
}
func startTakingScreenshots(every interval: TimeInterval) {
takeScreenshot()
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
self?.takeScreenshot()
}
}
public func addSanitizedElement(_ element: Sanitizable) {
#if DEBUG
DebugUtils.log("called add")
#endif
sanitizedElements.append(element)
}
public func removeSanitizedElement(_ element: Sanitizable) {
#if DEBUG
DebugUtils.log("called remove")
#endif
sanitizedElements.removeAll { $0 as AnyObject === element as AnyObject }
}
// MARK: - UI Capturing
func takeScreenshot() {
let window = UIApplication.shared.windows.first { $0.isKeyWindow }
let size = window?.frame.size ?? CGSize.zero
UIGraphicsBeginImageContextWithOptions(size, false, screenScale)
guard let context = UIGraphicsGetCurrentContext() else { return }
// Rendering current window in custom context
// 2nd option looks to be more precise
// window?.layer.render(in: context)
#warning("Can slow down the app depending on complexity of the UI tree")
window?.drawHierarchy(in: window?.bounds ?? CGRect.zero, afterScreenUpdates: false)
// MARK: sanitize
// Sanitizing sensitive elements
if isBlurMode {
let stripeWidth: CGFloat = 5.0
let stripeSpacing: CGFloat = 15.0
let stripeColor: UIColor = .gray.withAlphaComponent(0.7)
for element in sanitizedElements {
if let frame = element.frameInWindow {
let totalWidth = frame.size.width
let totalHeight = frame.size.height
let convertedFrame = CGRect(
x: frame.origin.x,
y: frame.origin.y,
width: frame.size.width,
height: frame.size.height
)
let cropFrame = CGRect(
x: frame.origin.x * screenScale,
y: frame.origin.y * screenScale,
width: frame.size.width * screenScale,
height: frame.size.height * screenScale
)
if let regionImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage?.cropping(to: cropFrame) {
let imageToBlur = UIImage(cgImage: regionImage, scale: screenScale, orientation: .up)
let blurredImage = imageToBlur.applyBlurWithRadius(blurRadius)
blurredImage?.draw(in: convertedFrame)
context.saveGState()
UIRectClip(convertedFrame)
// Draw diagonal lines within the clipped region
for x in stride(from: -totalHeight, to: totalWidth, by: stripeSpacing + stripeWidth) {
context.move(to: CGPoint(x: x + convertedFrame.minX, y: convertedFrame.minY))
context.addLine(to: CGPoint(x: x + totalHeight + convertedFrame.minX, y: totalHeight + convertedFrame.minY))
}
context.setLineWidth(stripeWidth)
stripeColor.setStroke()
context.strokePath()
context.restoreGState()
#if DEBUG
context.setStrokeColor(UIColor.black.cgColor)
context.setLineWidth(1)
context.stroke(convertedFrame)
#endif
}
} else {
removeSanitizedElement(element)
}
}
} else {
context.setFillColor(UIColor.blue.cgColor)
for element in sanitizedElements {
if let frame = element.frameInWindow {
context.fill(frame)
}
}
}
// Get the resulting image
if let image = UIGraphicsGetImageFromCurrentImageContext() {
if let compressedData = image.jpegData(compressionQuality: self.settings.imgCompression) {
screenshots.append(compressedData)
if screenshots.count >= 10 {
self.sendScreenshots()
}
}
}
UIGraphicsEndImageContext()
}
// Not using this because no idea how to sync it with the replay fps rn
//func onError() {
// takeScreenshot()
//}
// MARK: - sending screenshots
func sendScreenshots() {
guard let sessionId = NetworkManager.shared.sessionId else {
return
}
let localFilePath = "/Users/nikitamelnikov/Desktop/session/"
let desktopURL = URL(fileURLWithPath: localFilePath)
var archiveName = "\(sessionId)-\(String(format: "%06d", self.lastIndex)).tar.gz"
let archiveURL = desktopURL.appendingPathComponent(archiveName)
// Ensure the directory exists
let fileManager = FileManager.default
if !fileManager.fileExists(atPath: localFilePath) {
try? fileManager.createDirectory(at: desktopURL, withIntermediateDirectories: true, attributes: nil)
}
var combinedData = Data()
let images = screenshots
for (index, imageData) in screenshots.enumerated() {
combinedData.append(imageData)
#if DEBUG
let filename = "\(lastIndex)_\(index).jpeg"
let fileURL = desktopURL.appendingPathComponent(filename)
do {
try imageData.write(to: fileURL)
} catch {
DebugUtils.log("Unexpected error: \(error).")
}
#endif
}
#if DEBUG
DebugUtils.log("saved image files in \(localFilePath)")
#endif
messagesQueue.addOperation {
var entries: [TarEntry] = []
for imageData in images {
let filename = "\(String(format: "%06d", self.lastIndex)).jpeg"
var tarEntry = TarContainer.Entry(info: .init(name: filename, type: .regular), data: imageData)
tarEntry.info.permissions = Permissions(rawValue: 420)
tarEntry.info.creationTime = Date()
tarEntry.info.modificationTime = Date()
entries.append(tarEntry)
self.lastIndex+=1
}
do {
let gzData = try GzipArchive.archive(data: TarContainer.create(from: entries))
#if DEBUG
try gzData.write(to: archiveURL)
DebugUtils.log("Archive saved to \(archiveURL.path)")
MessageCollector.shared.sendImagesBatch(batch: gzData, fileName: archiveName)
#else
MessageCollector.shared.sendImagesBatch(batch: gzData, fileName: archiveName)
#endif
} catch {
DebugUtils.log("Error writing tar.gz data: \(error)")
}
}
screenshots.removeAll()
}
}
// MARK: making extensions for UI
struct SensitiveViewWrapperRepresentable: UIViewRepresentable {
@Binding var viewWrapper: SensitiveViewWrapper?
func makeUIView(context: Context) -> SensitiveViewWrapper {
let wrapper = SensitiveViewWrapper()
viewWrapper = wrapper
return wrapper
}
func updateUIView(_ uiView: SensitiveViewWrapper, context: Context) { }
}
struct SensitiveModifier: ViewModifier {
@State private var viewWrapper: SensitiveViewWrapper?
func body(content: Content) -> some View {
content
.background(SensitiveViewWrapperRepresentable(viewWrapper: $viewWrapper))
}
}
public extension View {
func sensitive() -> some View {
self.modifier(SensitiveModifier())
}
}
class SensitiveViewWrapper: UIView {
override func didMoveToSuperview() {
super.didMoveToSuperview()
if self.superview != nil {
ScreenshotManager.shared.addSanitizedElement(self)
} else {
ScreenshotManager.shared.removeSanitizedElement(self)
}
}
}
class SensitiveTextField: UITextField {
override func didMoveToWindow() {
super.didMoveToWindow()
if self.window != nil {
ScreenshotManager.shared.addSanitizedElement(self)
} else {
ScreenshotManager.shared.removeSanitizedElement(self)
}
}
}
// Protocol to make a UIView sanitizable
public protocol Sanitizable {
var frameInWindow: CGRect? { get }
}
func getCaptureSettings(for quality: RecordingQuality) -> (captureRate: Double, imgCompression: Double) {
switch quality {
case .Low:
return (captureRate: 1, imgCompression: 0.4)
case .Standard:
return (captureRate: 0.33, imgCompression: 0.5)
case .High:
return (captureRate: 0.20, imgCompression: 0.55)
}
}

View file

@ -1,11 +0,0 @@
import XCTest
@testable import ORTracker
final class ORTrackerTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
// XCTAssertEqual(ORTracker().text, "Hello, World!")
}
}