@sentry/replay
Advanced tools
Comparing version 0.2.0-10 to 0.2.0-11
import * as Sentry from '@sentry/browser'; | ||
import { record } from 'rrweb'; | ||
import { logger as logger$1 } from '@sentry/utils'; | ||
import { getCurrentHub, captureEvent } from '@sentry/browser'; | ||
import { logger as logger$1, uuid4, addInstrumentationHandler, htmlTreeAsString } from '@sentry/utils'; | ||
import { record as record$1 } from 'rrweb'; | ||
@@ -158,3 +159,3 @@ /*! ***************************************************************************** | ||
// @ts-expect-error Not sure why this errors, Node should be correct (Argument of type 'Node' is not assignable to parameter of type 'INode') | ||
nodeId: record.mirror.getId(entry.element), | ||
nodeId: record$1.mirror.getId(entry.element), | ||
}, | ||
@@ -250,21 +251,14 @@ }; | ||
var currentDate = new Date().getTime(); | ||
// Create root replay event, this is where attachments will be saved | ||
var transaction = Sentry.getCurrentHub().startTransaction({ | ||
name: ROOT_REPLAY_NAME, | ||
tags: { | ||
isReplayRoot: 'yes', | ||
}, | ||
}); | ||
// We have to finish the transaction to get an event ID to be able to | ||
// upload an attachment for that event | ||
// @ts-expect-error This returns an eventId (string), but is not typed as such | ||
var id = transaction.finish(); | ||
logger.log("Creating new session: ".concat(id)); | ||
var hub = getCurrentHub(); | ||
var session = { | ||
id: id, | ||
spanId: transaction.spanId, | ||
traceId: transaction.traceId, | ||
id: uuid4(), | ||
started: currentDate, | ||
lastActivity: currentDate, | ||
sequenceId: 0, | ||
}; | ||
hub.captureEvent({ | ||
message: ROOT_REPLAY_NAME, | ||
tags: { sequenceId: session.sequenceId }, | ||
}, { event_id: session.id }); | ||
logger.log("Creating new session: ".concat(session.id)); | ||
if (stickySession) { | ||
@@ -330,2 +324,72 @@ saveSession(session); | ||
function addInstrumentationListeners(scope, replay) { | ||
scope.addScopeListener(scopeListenerCallback.bind(replay)); | ||
addInstrumentationHandler('dom', domCallback.bind(replay)); | ||
addInstrumentationHandler('xhr', xhrCallback.bind(replay)); | ||
addInstrumentationHandler('fetch', fetchCallback.bind(replay)); | ||
} | ||
function scopeListenerCallback(scope) { | ||
//@ts-expect-error using private val | ||
var newBreadcrumb = scope._breadcrumbs[scope._breadcrumbs.length - 1]; | ||
if (['fetch', 'xhr', 'sentry.event'].includes(newBreadcrumb.category) || | ||
newBreadcrumb.category.startsWith('ui.')) { | ||
return; | ||
} | ||
this.breadcrumbs.push(__assign({ type: 'default' }, newBreadcrumb)); | ||
} | ||
function domCallback(handlerData) { | ||
// Taken from https://github.com/getsentry/sentry-javascript/blob/master/packages/browser/src/integrations/breadcrumbs.ts#L112 | ||
var target; | ||
var targetNode; | ||
// Accessing event.target can throw (see getsentry/raven-js#838, #768) | ||
try { | ||
targetNode = | ||
handlerData.event.target || | ||
handlerData.event; | ||
target = htmlTreeAsString(targetNode); | ||
} | ||
catch (e) { | ||
target = '<unknown>'; | ||
} | ||
if (target.length === 0) { | ||
return; | ||
} | ||
this.breadcrumbs.push({ | ||
timestamp: new Date().getTime() / 1000, | ||
type: 'default', | ||
category: "ui.".concat(handlerData.name), | ||
message: target, | ||
data: { | ||
// @ts-expect-error Not sure why this errors, Node should be correct (Argument of type 'Node' is not assignable to parameter of type 'INode') | ||
nodeId: targetNode ? record.mirror.getId(targetNode) : undefined, | ||
}, | ||
}); | ||
} | ||
function xhrCallback(handlerData) { | ||
// TODO: add status code into data, etc. | ||
if (handlerData.startTimestamp) { | ||
handlerData.xhr.__sentry_xhr__.startTimestamp = handlerData.startTimestamp; | ||
} | ||
if (handlerData.endTimestamp) { | ||
this.spans.push({ | ||
description: handlerData.args[1], | ||
op: handlerData.args[0], | ||
startTimestamp: handlerData.xhr.__sentry_xhr__.startTimestamp / 1000 || | ||
handlerData.endTimestamp / 1000.0, | ||
endTimestamp: handlerData.endTimestamp / 1000.0, | ||
}); | ||
} | ||
} | ||
function fetchCallback(handlerData) { | ||
// TODO: add status code into data, etc. | ||
if (handlerData.endTimestamp) { | ||
this.spans.push({ | ||
description: handlerData.args[1], | ||
op: handlerData.args[0], | ||
startTimestamp: handlerData.startTimestamp / 1000, | ||
endTimestamp: handlerData.endTimestamp / 1000, | ||
}); | ||
} | ||
} | ||
var SentryReplay = /** @class */ (function () { | ||
@@ -347,2 +411,3 @@ function SentryReplay(_a) { | ||
this.performanceEvents = []; | ||
this.breadcrumbs = []; | ||
/** | ||
@@ -353,2 +418,3 @@ * The timestamp of the first event since the last flush. | ||
this.initialEventTimestampSinceFlush = null; | ||
this.replaySpans = []; | ||
this.performanceObserver = null; | ||
@@ -404,6 +470,6 @@ /** | ||
SentryReplay.attachmentUrlFromDsn = function (dsn, eventId) { | ||
var host = dsn.host, projectId = dsn.projectId, protocol = dsn.protocol, user = dsn.user; | ||
var host = dsn.host, projectId = dsn.projectId, protocol = dsn.protocol, publicKey = dsn.publicKey; | ||
var port = dsn.port !== '' ? ":".concat(dsn.port) : ''; | ||
var path = dsn.path !== '' ? "/".concat(dsn.path) : ''; | ||
return "".concat(protocol, "://").concat(host).concat(port).concat(path, "/api/").concat(projectId, "/events/").concat(eventId, "/attachments/?sentry_key=").concat(user, "&sentry_version=7&sentry_client=replay"); | ||
return "".concat(protocol, "://").concat(host).concat(port).concat(path, "/api/").concat(projectId, "/events/").concat(eventId, "/attachments/?sentry_key=").concat(publicKey, "&sentry_version=7&sentry_client=replay"); | ||
}; | ||
@@ -424,2 +490,5 @@ SentryReplay.prototype.setupOnce = function () { | ||
var _this = this; | ||
var hub = Sentry.getCurrentHub(); | ||
var scope = hub.getStackTop().scope; | ||
addInstrumentationListeners(scope, this); | ||
this.loadSession({ expiry: SESSION_IDLE_DURATION }); | ||
@@ -440,5 +509,3 @@ // If there is no session, then something bad has happened - can't continue | ||
}); | ||
// not fully initialized and the event will not get properly sent to Sentry | ||
this.createReplayEvent(); | ||
record(__assign(__assign({}, this.rrwebRecordOptions), { emit: function (event, isCheckout) { | ||
record$1(__assign(__assign({}, this.rrwebRecordOptions), { emit: function (event, isCheckout) { | ||
// We want to batch uploads of replay events. Save events only if | ||
@@ -551,24 +618,5 @@ // `<uploadMinDelay>` milliseconds have elapsed since the last event | ||
logger.log('Taking full rrweb snapshot'); | ||
record.takeFullSnapshot(true); | ||
record$1.takeFullSnapshot(true); | ||
}; | ||
/** | ||
* This is our pseudo replay event disguised as a transaction. It will be | ||
* used to store performance entries and breadcrumbs for every incremental | ||
* replay event. | ||
**/ | ||
SentryReplay.prototype.createReplayEvent = function () { | ||
var _this = this; | ||
logger.log('CreateReplayEvent rootReplayId', this.session.id); | ||
this.replayEvent = Sentry.getCurrentHub().startTransaction({ | ||
name: REPLAY_EVENT_NAME, | ||
parentSpanId: this.session.spanId, | ||
traceId: this.session.traceId, | ||
tags: { | ||
replayId: this.session.id, | ||
}, | ||
}); | ||
Sentry.configureScope(function (scope) { return scope.setSpan(_this.replayEvent); }); | ||
return this.replayEvent; | ||
}; | ||
/** | ||
* Create a span for each performance entry. The parent transaction is `this.replayEvent`. | ||
@@ -579,11 +627,10 @@ */ | ||
entries.forEach(function (_a) { | ||
var _b; | ||
var type = _a.type, start = _a.start, end = _a.end, name = _a.name, data = _a.data; | ||
var span = (_b = _this.replayEvent) === null || _b === void 0 ? void 0 : _b.startChild({ | ||
_this.replaySpans.push({ | ||
op: type, | ||
description: name, | ||
startTimestamp: start, | ||
endTimestamp: end, | ||
data: data, | ||
}); | ||
span.finish(end); | ||
}); | ||
@@ -607,3 +654,2 @@ }; | ||
} | ||
// This current implementation is to create spans on the transaction referenced in `this.replayEvent` | ||
this.createPerformanceSpans(entryEvents); | ||
@@ -635,3 +681,2 @@ }; | ||
SentryReplay.prototype.finishReplayEvent = function () { | ||
var _a; | ||
if (!this.checkAndHandleExpiredSession()) { | ||
@@ -644,2 +689,11 @@ logger.error(new Error('Attempting to finish replay event after session expired.')); | ||
} | ||
// TEMP: keep sending a replay event just for the duration | ||
captureEvent({ | ||
message: "".concat(REPLAY_EVENT_NAME, "-").concat(uuid4().substring(16)), | ||
tags: { | ||
replayId: this.session.id, | ||
sequenceId: this.session.sequenceId++, | ||
}, | ||
}); | ||
this.addPerformanceEntries(); | ||
this.sendReplay(this.session.id); | ||
@@ -649,7 +703,2 @@ this.initialEventTimestampSinceFlush = null; | ||
this.session.lastActivity = new Date().getTime(); | ||
// include performance entries | ||
this.addPerformanceEntries(); | ||
// Close out existing replay event and create a new one | ||
(_a = this.replayEvent) === null || _a === void 0 ? void 0 : _a.setStatus('ok').finish(); | ||
this.createReplayEvent(); | ||
}; | ||
@@ -665,9 +714,14 @@ /** | ||
*/ | ||
SentryReplay.prototype.sendReplayRequest = function (endpoint, events) { | ||
SentryReplay.prototype.sendReplayRequest = function (_a) { | ||
var endpoint = _a.endpoint, events = _a.events, replaySpans = _a.replaySpans, breadcrumbs = _a.breadcrumbs; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var stringifiedPayload, formData; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
stringifiedPayload = JSON.stringify({ events: events }); | ||
stringifiedPayload = JSON.stringify({ | ||
recording: events, | ||
replaySpans: replaySpans, | ||
breadcrumbs: breadcrumbs, | ||
}); | ||
formData = new FormData(); | ||
@@ -690,3 +744,3 @@ formData.append('rrweb', new Blob([stringifiedPayload], { | ||
case 1: | ||
_a.sent(); | ||
_b.sent(); | ||
return [2 /*return*/]; | ||
@@ -702,3 +756,3 @@ } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var events, client, endpoint, ex_1; | ||
var events, replaySpans, breadcrumbs, client, endpoint, ex_1; | ||
return __generator(this, function (_a) { | ||
@@ -711,2 +765,6 @@ switch (_a.label) { | ||
events = this.events; | ||
replaySpans = this.replaySpans; | ||
breadcrumbs = this.breadcrumbs; | ||
this.replaySpans = []; | ||
this.breadcrumbs = []; | ||
this.events = []; | ||
@@ -718,3 +776,8 @@ client = Sentry.getCurrentHub().getClient(); | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, this.sendReplayRequest(endpoint, events)]; | ||
return [4 /*yield*/, this.sendReplayRequest({ | ||
endpoint: endpoint, | ||
events: events, | ||
replaySpans: replaySpans, | ||
breadcrumbs: breadcrumbs, | ||
})]; | ||
case 2: | ||
@@ -721,0 +784,0 @@ _a.sent(); |
@@ -6,4 +6,4 @@ 'use strict'; | ||
var Sentry = require('@sentry/browser'); | ||
var utils = require('@sentry/utils'); | ||
var rrweb = require('rrweb'); | ||
var utils = require('@sentry/utils'); | ||
@@ -274,21 +274,14 @@ function _interopNamespace(e) { | ||
var currentDate = new Date().getTime(); | ||
// Create root replay event, this is where attachments will be saved | ||
var transaction = Sentry__namespace.getCurrentHub().startTransaction({ | ||
name: ROOT_REPLAY_NAME, | ||
tags: { | ||
isReplayRoot: 'yes', | ||
}, | ||
}); | ||
// We have to finish the transaction to get an event ID to be able to | ||
// upload an attachment for that event | ||
// @ts-expect-error This returns an eventId (string), but is not typed as such | ||
var id = transaction.finish(); | ||
logger.log("Creating new session: ".concat(id)); | ||
var hub = Sentry.getCurrentHub(); | ||
var session = { | ||
id: id, | ||
spanId: transaction.spanId, | ||
traceId: transaction.traceId, | ||
id: utils.uuid4(), | ||
started: currentDate, | ||
lastActivity: currentDate, | ||
sequenceId: 0, | ||
}; | ||
hub.captureEvent({ | ||
message: ROOT_REPLAY_NAME, | ||
tags: { sequenceId: session.sequenceId }, | ||
}, { event_id: session.id }); | ||
logger.log("Creating new session: ".concat(session.id)); | ||
if (stickySession) { | ||
@@ -354,2 +347,72 @@ saveSession(session); | ||
function addInstrumentationListeners(scope, replay) { | ||
scope.addScopeListener(scopeListenerCallback.bind(replay)); | ||
utils.addInstrumentationHandler('dom', domCallback.bind(replay)); | ||
utils.addInstrumentationHandler('xhr', xhrCallback.bind(replay)); | ||
utils.addInstrumentationHandler('fetch', fetchCallback.bind(replay)); | ||
} | ||
function scopeListenerCallback(scope) { | ||
//@ts-expect-error using private val | ||
var newBreadcrumb = scope._breadcrumbs[scope._breadcrumbs.length - 1]; | ||
if (['fetch', 'xhr', 'sentry.event'].includes(newBreadcrumb.category) || | ||
newBreadcrumb.category.startsWith('ui.')) { | ||
return; | ||
} | ||
this.breadcrumbs.push(__assign({ type: 'default' }, newBreadcrumb)); | ||
} | ||
function domCallback(handlerData) { | ||
// Taken from https://github.com/getsentry/sentry-javascript/blob/master/packages/browser/src/integrations/breadcrumbs.ts#L112 | ||
var target; | ||
var targetNode; | ||
// Accessing event.target can throw (see getsentry/raven-js#838, #768) | ||
try { | ||
targetNode = | ||
handlerData.event.target || | ||
handlerData.event; | ||
target = utils.htmlTreeAsString(targetNode); | ||
} | ||
catch (e) { | ||
target = '<unknown>'; | ||
} | ||
if (target.length === 0) { | ||
return; | ||
} | ||
this.breadcrumbs.push({ | ||
timestamp: new Date().getTime() / 1000, | ||
type: 'default', | ||
category: "ui.".concat(handlerData.name), | ||
message: target, | ||
data: { | ||
// @ts-expect-error Not sure why this errors, Node should be correct (Argument of type 'Node' is not assignable to parameter of type 'INode') | ||
nodeId: targetNode ? record.mirror.getId(targetNode) : undefined, | ||
}, | ||
}); | ||
} | ||
function xhrCallback(handlerData) { | ||
// TODO: add status code into data, etc. | ||
if (handlerData.startTimestamp) { | ||
handlerData.xhr.__sentry_xhr__.startTimestamp = handlerData.startTimestamp; | ||
} | ||
if (handlerData.endTimestamp) { | ||
this.spans.push({ | ||
description: handlerData.args[1], | ||
op: handlerData.args[0], | ||
startTimestamp: handlerData.xhr.__sentry_xhr__.startTimestamp / 1000 || | ||
handlerData.endTimestamp / 1000.0, | ||
endTimestamp: handlerData.endTimestamp / 1000.0, | ||
}); | ||
} | ||
} | ||
function fetchCallback(handlerData) { | ||
// TODO: add status code into data, etc. | ||
if (handlerData.endTimestamp) { | ||
this.spans.push({ | ||
description: handlerData.args[1], | ||
op: handlerData.args[0], | ||
startTimestamp: handlerData.startTimestamp / 1000, | ||
endTimestamp: handlerData.endTimestamp / 1000, | ||
}); | ||
} | ||
} | ||
var SentryReplay = /** @class */ (function () { | ||
@@ -371,2 +434,3 @@ function SentryReplay(_a) { | ||
this.performanceEvents = []; | ||
this.breadcrumbs = []; | ||
/** | ||
@@ -377,2 +441,3 @@ * The timestamp of the first event since the last flush. | ||
this.initialEventTimestampSinceFlush = null; | ||
this.replaySpans = []; | ||
this.performanceObserver = null; | ||
@@ -428,6 +493,6 @@ /** | ||
SentryReplay.attachmentUrlFromDsn = function (dsn, eventId) { | ||
var host = dsn.host, projectId = dsn.projectId, protocol = dsn.protocol, user = dsn.user; | ||
var host = dsn.host, projectId = dsn.projectId, protocol = dsn.protocol, publicKey = dsn.publicKey; | ||
var port = dsn.port !== '' ? ":".concat(dsn.port) : ''; | ||
var path = dsn.path !== '' ? "/".concat(dsn.path) : ''; | ||
return "".concat(protocol, "://").concat(host).concat(port).concat(path, "/api/").concat(projectId, "/events/").concat(eventId, "/attachments/?sentry_key=").concat(user, "&sentry_version=7&sentry_client=replay"); | ||
return "".concat(protocol, "://").concat(host).concat(port).concat(path, "/api/").concat(projectId, "/events/").concat(eventId, "/attachments/?sentry_key=").concat(publicKey, "&sentry_version=7&sentry_client=replay"); | ||
}; | ||
@@ -448,2 +513,5 @@ SentryReplay.prototype.setupOnce = function () { | ||
var _this = this; | ||
var hub = Sentry__namespace.getCurrentHub(); | ||
var scope = hub.getStackTop().scope; | ||
addInstrumentationListeners(scope, this); | ||
this.loadSession({ expiry: SESSION_IDLE_DURATION }); | ||
@@ -464,4 +532,2 @@ // If there is no session, then something bad has happened - can't continue | ||
}); | ||
// not fully initialized and the event will not get properly sent to Sentry | ||
this.createReplayEvent(); | ||
rrweb.record(__assign(__assign({}, this.rrwebRecordOptions), { emit: function (event, isCheckout) { | ||
@@ -578,21 +644,2 @@ // We want to batch uploads of replay events. Save events only if | ||
/** | ||
* This is our pseudo replay event disguised as a transaction. It will be | ||
* used to store performance entries and breadcrumbs for every incremental | ||
* replay event. | ||
**/ | ||
SentryReplay.prototype.createReplayEvent = function () { | ||
var _this = this; | ||
logger.log('CreateReplayEvent rootReplayId', this.session.id); | ||
this.replayEvent = Sentry__namespace.getCurrentHub().startTransaction({ | ||
name: REPLAY_EVENT_NAME, | ||
parentSpanId: this.session.spanId, | ||
traceId: this.session.traceId, | ||
tags: { | ||
replayId: this.session.id, | ||
}, | ||
}); | ||
Sentry__namespace.configureScope(function (scope) { return scope.setSpan(_this.replayEvent); }); | ||
return this.replayEvent; | ||
}; | ||
/** | ||
* Create a span for each performance entry. The parent transaction is `this.replayEvent`. | ||
@@ -603,11 +650,10 @@ */ | ||
entries.forEach(function (_a) { | ||
var _b; | ||
var type = _a.type, start = _a.start, end = _a.end, name = _a.name, data = _a.data; | ||
var span = (_b = _this.replayEvent) === null || _b === void 0 ? void 0 : _b.startChild({ | ||
_this.replaySpans.push({ | ||
op: type, | ||
description: name, | ||
startTimestamp: start, | ||
endTimestamp: end, | ||
data: data, | ||
}); | ||
span.finish(end); | ||
}); | ||
@@ -631,3 +677,2 @@ }; | ||
} | ||
// This current implementation is to create spans on the transaction referenced in `this.replayEvent` | ||
this.createPerformanceSpans(entryEvents); | ||
@@ -659,3 +704,2 @@ }; | ||
SentryReplay.prototype.finishReplayEvent = function () { | ||
var _a; | ||
if (!this.checkAndHandleExpiredSession()) { | ||
@@ -668,2 +712,11 @@ logger.error(new Error('Attempting to finish replay event after session expired.')); | ||
} | ||
// TEMP: keep sending a replay event just for the duration | ||
Sentry.captureEvent({ | ||
message: "".concat(REPLAY_EVENT_NAME, "-").concat(utils.uuid4().substring(16)), | ||
tags: { | ||
replayId: this.session.id, | ||
sequenceId: this.session.sequenceId++, | ||
}, | ||
}); | ||
this.addPerformanceEntries(); | ||
this.sendReplay(this.session.id); | ||
@@ -673,7 +726,2 @@ this.initialEventTimestampSinceFlush = null; | ||
this.session.lastActivity = new Date().getTime(); | ||
// include performance entries | ||
this.addPerformanceEntries(); | ||
// Close out existing replay event and create a new one | ||
(_a = this.replayEvent) === null || _a === void 0 ? void 0 : _a.setStatus('ok').finish(); | ||
this.createReplayEvent(); | ||
}; | ||
@@ -689,9 +737,14 @@ /** | ||
*/ | ||
SentryReplay.prototype.sendReplayRequest = function (endpoint, events) { | ||
SentryReplay.prototype.sendReplayRequest = function (_a) { | ||
var endpoint = _a.endpoint, events = _a.events, replaySpans = _a.replaySpans, breadcrumbs = _a.breadcrumbs; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var stringifiedPayload, formData; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
stringifiedPayload = JSON.stringify({ events: events }); | ||
stringifiedPayload = JSON.stringify({ | ||
recording: events, | ||
replaySpans: replaySpans, | ||
breadcrumbs: breadcrumbs, | ||
}); | ||
formData = new FormData(); | ||
@@ -714,3 +767,3 @@ formData.append('rrweb', new Blob([stringifiedPayload], { | ||
case 1: | ||
_a.sent(); | ||
_b.sent(); | ||
return [2 /*return*/]; | ||
@@ -726,3 +779,3 @@ } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var events, client, endpoint, ex_1; | ||
var events, replaySpans, breadcrumbs, client, endpoint, ex_1; | ||
return __generator(this, function (_a) { | ||
@@ -735,2 +788,6 @@ switch (_a.label) { | ||
events = this.events; | ||
replaySpans = this.replaySpans; | ||
breadcrumbs = this.breadcrumbs; | ||
this.replaySpans = []; | ||
this.breadcrumbs = []; | ||
this.events = []; | ||
@@ -742,3 +799,8 @@ client = Sentry__namespace.getCurrentHub().getClient(); | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, this.sendReplayRequest(endpoint, events)]; | ||
return [4 /*yield*/, this.sendReplayRequest({ | ||
endpoint: endpoint, | ||
events: events, | ||
replaySpans: replaySpans, | ||
breadcrumbs: breadcrumbs, | ||
})]; | ||
case 2: | ||
@@ -745,0 +807,0 @@ _a.sent(); |
{ | ||
"name": "@sentry/replay", | ||
"version": "0.2.0-10", | ||
"version": "0.2.0-11", | ||
"description": "User replays for Sentry", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
159367
1773