diff --git a/api_server/CHANGELOG.md b/api_server/CHANGELOG.md
index f92c42c27..a63bdfc05 100644
--- a/api_server/CHANGELOG.md
+++ b/api_server/CHANGELOG.md
@@ -1,10 +1,11 @@
# Change Log
-## [15.4.0] - 2024-4-4
+## [15.4.0] - 2024-4-17
### Changed
- Updates to telemetry events
+- A/B test open-in-IDE redirect pages
## [15.3.0] - 2024-3-14
diff --git a/api_server/bin/api_server.js b/api_server/bin/api_server.js
index a2f954d33..8c28a5882 100755
--- a/api_server/bin/api_server.js
+++ b/api_server/bin/api_server.js
@@ -35,7 +35,8 @@ const DataCollections = {
msteams_conversations: require(ModuleDirectory + '/msteams_conversations/msteams_conversation'),
msteams_states: require(ModuleDirectory + '/msteams_states/msteams_state'),
msteams_teams: require(ModuleDirectory + '/msteams_teams/msteams_team'),
- reposByCommitHash: require(ModuleDirectory + '/repos/repo_by_commit_hash')
+ reposByCommitHash: require(ModuleDirectory + '/repos/repo_by_commit_hash'),
+ entities: require(ModuleDirectory + '/entities/entity')
};
// establish our mongo collections, these include our DataCollections, but
@@ -67,7 +68,6 @@ const MongoCollections = Object.keys(DataCollections).concat([
// changes to Config will be available globally via the /config/writeable.js module
const Config = await ApiConfig.loadPreferredConfig({ wait: true });
-Config.telemetry.segment.telemetryEndpoint = 'https://taxonomy-enforcer.service.newrelic.com'; // for now
// establish our logger
const Logger = new SimpleFileLogger(Config.apiServer.logger);
diff --git a/api_server/bin/ensure-indexes.js b/api_server/bin/ensure-indexes.js
index bb102fbf0..bbe239daf 100755
--- a/api_server/bin/ensure-indexes.js
+++ b/api_server/bin/ensure-indexes.js
@@ -32,7 +32,8 @@ const AllModuleIndexes = {
msteams_teams: require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/msteams_teams/indexes'),
reposByCommitHash: require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/repos/repo_by_commit_hash_indexes'),
gitLensUsers: require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/users/gitlens_user_indexes'),
- newRelicOrgs: require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/newrelic_comments/new_relic_org_indexes')
+ newRelicOrgs: require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/newrelic_comments/new_relic_org_indexes'),
+ entities: require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/entities/indexes')
};
const AllFinished = {
diff --git a/api_server/config/dev-secrets.json b/api_server/config/dev-secrets.json
index b8b866fc9..4013b7151 100644
--- a/api_server/config/dev-secrets.json
+++ b/api_server/config/dev-secrets.json
@@ -47,6 +47,7 @@
"STORAGE_MONGO_URL",
"TELEMETRY_SEGMENT_TOKEN",
"TELEMETRY_SEGMENT_WEB_TOKEN",
+ "TELEMETRY_ENDPOINT",
"UNIVERSAL_SECRETS_TELEMETRY",
"UNIVERSAL_SECRETS_TELEMETRY",
"SHARED_SECRETS_COMMENT_ENGINE",
diff --git a/api_server/lib/test_base/codestream_api_test.js b/api_server/lib/test_base/codestream_api_test.js
index cec9c1ca3..491fe11d1 100644
--- a/api_server/lib/test_base/codestream_api_test.js
+++ b/api_server/lib/test_base/codestream_api_test.js
@@ -15,6 +15,7 @@ const RandomCodemarkFactory = require(process.env.CSSVC_BACKEND_ROOT + '/api_ser
const RandomReviewFactory = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/reviews/test/random_review_factory');
const RandomCodeErrorFactory = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/code_errors/test/random_code_error_factory');
const RandomNRCommentFactory = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/newrelic_comments/test/random_nr_comment_factory');
+const RandomEntityFactory = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/entities/test/random_entity_factory');
const Assert = require('assert');
const BoundAsync = require(process.env.CSSVC_BACKEND_ROOT + '/shared/server_utils/bound_async');
const TestTeamCreator = require('./test_team_creator');
@@ -75,7 +76,10 @@ class CodeStreamAPITest extends APIRequestTest {
userFactory: this.userFactory,
codeErrorFactory: this.codeErrorFactory
});
-
+ this.entityFactory = new RandomEntityFactory({
+ apiRequester: this
+ });
+
this.userOptions = {
numRegistered: 2,
numUnregistered: 0,
diff --git a/api_server/lib/util/deactivator.js b/api_server/lib/util/deactivator.js
index 21c05f144..2b7151565 100644
--- a/api_server/lib/util/deactivator.js
+++ b/api_server/lib/util/deactivator.js
@@ -4,9 +4,10 @@ const ObjectId = require('mongodb').ObjectId;
const UserIndexes = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/users/indexes');
const RepoIndexes = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/repos/indexes');
const StreamIndexes = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/streams/indexes');
+const EntityIndexes = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/entities/indexes');
-const COLLECTIONS = ['companies', 'teams', 'repos', 'users', 'streams', 'posts', 'codemarks', 'reviews', 'codeErrors', 'markers', 'markerLocations'];
-const COLLECTIONS_FOR_TEAM = ['streams', 'posts', 'codemarks', 'reviews', 'codeErrors', 'markers', 'markerLocations'];
+const COLLECTIONS = ['companies', 'teams', 'repos', 'users', 'streams', 'posts', 'codemarks', 'reviews', 'codeErrors', 'markers', 'markerLocations', 'entities'];
+const COLLECTIONS_FOR_TEAM = ['streams', 'posts', 'codemarks', 'reviews', 'codeErrors', 'markers', 'markerLocations', 'entities'];
class Deleter {
diff --git a/api_server/lib/util/deleter.js b/api_server/lib/util/deleter.js
index 2c8c42060..ec3205650 100644
--- a/api_server/lib/util/deleter.js
+++ b/api_server/lib/util/deleter.js
@@ -3,8 +3,9 @@ const ApiConfig = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/config/c
const ObjectId = require('mongodb').ObjectId;
const UserIndexes = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/users/indexes');
const RepoIndexes = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/repos/indexes');
+const EntityIndexes = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/entities/indexes');
-const COLLECTIONS = ['companies', 'teams', 'repos', 'users', 'streams', 'posts', 'codemarks', 'markers', 'markerLocations'];
+const COLLECTIONS = ['companies', 'teams', 'repos', 'users', 'streams', 'posts', 'codemarks', 'markers', 'markerLocations', 'entities'];
class Deleter {
@@ -25,6 +26,7 @@ class Deleter {
await this.deleteCodemarks();
await this.deleteMarkers();
await this.deleteMarkerLocations();
+ await this.deleteEntities();
}
async openMongoClient () {
@@ -365,6 +367,19 @@ class Deleter {
throw `unable to delete marker locations: ${JSON.stringify(error)}`;
}
}
+
+ async deleteEntities () {
+ this.logger.log(`Deleting entities in team ${this.teamId}...`);
+ try {
+ await this.mongoClient.mongoCollections.entities.deleteByQuery(
+ { teamId: this.teamId },
+ { overrideHintRequired: true }
+ );
+ }
+ catch (error) {
+ throw `unable to delete entities: ${JSON.stringify(error)}`;
+ }
+ }
}
module.exports = Deleter;
diff --git a/api_server/lib/util/restful/post_request.js b/api_server/lib/util/restful/post_request.js
index 1b2431886..b7e8046e3 100644
--- a/api_server/lib/util/restful/post_request.js
+++ b/api_server/lib/util/restful/post_request.js
@@ -25,7 +25,9 @@ class PostRequest extends RestfulRequest {
// after the request has been processed and response returned to the client....
async postProcess () {
- await this.creator.postCreate();
+ if (this.creator) {
+ await this.creator.postCreate();
+ }
}
// describe this route for help
diff --git a/api_server/modules/analytics/analytics_client.js b/api_server/modules/analytics/analytics_client.js
index f8eceb193..f1d3b613f 100644
--- a/api_server/modules/analytics/analytics_client.js
+++ b/api_server/modules/analytics/analytics_client.js
@@ -37,15 +37,15 @@ class AnalyticsClient {
properties: data,
messageId: UUID(),
timestamp: new Date(),
- type: "track"
+ type: "track",
+ anonymousId: UUID(),
};
+ data.session_id = data.session_id || UUID();
+
const nrUserId = options.user ? options.user.get('nrUserId') : options.nrUserId;
if (nrUserId) {
trackData.userId = nrUserId; //userId;
}
- if (options.anonymousId) {
- trackData.anonymousId = options.anonymousId;
- }
if (this._requestSaysToTestTracking(options)) {
// we received a header in the request asking us to divert this tracking event
@@ -59,6 +59,10 @@ class AnalyticsClient {
}
//this.segment.track(trackData);
+ if (options.request) {
+ options.request.log('TRACKING TO ' + this.config.telemetryEndpoint + '/events');
+ options.request.log('trackData: ' + JSON.stringify(trackData));
+ }
Fetch(
this.config.telemetryEndpoint + '/events',
{
diff --git a/api_server/modules/apiweb/apiweb.js b/api_server/modules/apiweb/apiweb.js
index e4f79b787..d6feca9e5 100644
--- a/api_server/modules/apiweb/apiweb.js
+++ b/api_server/modules/apiweb/apiweb.js
@@ -102,6 +102,8 @@ class Web extends APIServerModule {
return JSON.stringify(obj);
});
+ Handlebars.registerHelper('eq', (a, b) => a == b);
+
// Not in use, but might be useful in the future
/**
* Format string with data
diff --git a/api_server/modules/apiweb/newrelic_ide_redirect_request.js b/api_server/modules/apiweb/newrelic_ide_redirect_request.js
index ad0b466a7..4bffd7ef5 100644
--- a/api_server/modules/apiweb/newrelic_ide_redirect_request.js
+++ b/api_server/modules/apiweb/newrelic_ide_redirect_request.js
@@ -4,7 +4,12 @@
const IdeRedirectRequest = require('./ide_redirect_request');
const { defaultCookieName, ides} = require('./config');
+
class NewRelicIdeRedirectRequest extends IdeRedirectRequest {
+ constructor(options) {
+ super(options);
+ this.abTest = Math.random() < 0.5 ? "feature_tabs" : "feature_bullets";
+ }
async prepareTemplateProps () {
this.redirectType = this.request.params.type.toLowerCase();
@@ -48,6 +53,7 @@ class NewRelicIdeRedirectRequest extends IdeRedirectRequest {
}
const launcherModel = this.createLauncherModel('');
this.templateProps = {
+ abTest: this.abTest,
pageType,
pageWhat,
analyticsContentType,
@@ -107,6 +113,7 @@ class NewRelicIdeRedirectRequest extends IdeRedirectRequest {
}
}).bind(this))();
const result = {
+ abTest: this.abTest,
environment,
ides: ides,
src: decodeURIComponent(this.parsedPayload.src || ''),
diff --git a/api_server/modules/apiweb/styles/web.css b/api_server/modules/apiweb/styles/web.css
index d6b1d89d9..b6024021d 100644
--- a/api_server/modules/apiweb/styles/web.css
+++ b/api_server/modules/apiweb/styles/web.css
@@ -192,6 +192,42 @@ h6 {
color: white!important;
text-decoration: underline;
}
+.content-body-container {
+ max-width: 80%;
+ margin: 0 auto;
+ text-align: left;
+}
+.content-body-section-header {
+ margin-top: 28px;
+}
+.content-body-header {
+ font-size: xx-large;
+}
+.content-body-tab-container {
+ margin-top: 32px;
+ margin-bottom: 10px;
+ border-bottom: 1px solid #3c3c3c;
+ display: flex;
+ justify-content: space-between;
+}
+.content-body-tab-header {
+ font-size: larger;
+ cursor: pointer;
+ display: inline-block;
+ text-align: center;
+ padding-bottom: 5px;
+ margin-bottom: -1px;
+}
+.content-body-copy {
+ margin-bottom: 12px;
+}
+.content-body-tab {
+ display: none;
+}
+.content-active-tab {
+ /* display: block !important; */
+ border-bottom: 1px solid;
+}
.box-border {
margin-top: 1rem;
padding: 1.5rem;
@@ -550,7 +586,7 @@ hr {
.interstitial-detail-wrapper {
padding: 30px;
- margin: 0 0 40px 0;
+ margin: 10px 0 10px 0;
max-width: 704px;
margin-right: auto;
margin-left: auto;
@@ -637,8 +673,21 @@ hr {
.observability-gif {
width: 100%;
+ border-radius: 8px;
+ margin-top: 10px;
}
+@media (max-width: 768px) {
+ .content-body-tab-icon {
+ display: none !important;
+ }
+ .content-body-tab-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
@media (min-width: 1px) {
.btn-block-wrap {
width: 100%;
@@ -767,11 +816,11 @@ span.dropdown-item:hover {
}
.navbar-light.bg-light {
background: #fff !important;
- margin: 0 0 80px 0;
+ margin: 0 0 0 0;
}
.navbar-dark.bg-dark {
/* background: #1D252C !important; */
- margin: 0 0 80px 0;
+ margin: 0 0 0 0;
border-bottom: 1px solid #2a3036;
padding: 15px 0 15px 0;
}
@@ -796,10 +845,11 @@ span.dropdown-item:hover {
}
.form-control {
border: 1px solid #383838 !important;
+ padding: 6px 0 0 0 !important;
}
.btn-light.form-control {
border: 1px solid rgb(227, 228, 228) !important;
- height: 50px;
+ /* height: 50px; */
}
.lines-added {
color: #66aa66;
diff --git a/api_server/modules/apiweb/templates/ide_redirect.hbs b/api_server/modules/apiweb/templates/ide_redirect.hbs
index 3706e353a..2cd280777 100644
--- a/api_server/modules/apiweb/templates/ide_redirect.hbs
+++ b/api_server/modules/apiweb/templates/ide_redirect.hbs
@@ -8,17 +8,17 @@
{{> partial_html_head }}
+
\ No newline at end of file
diff --git a/api_server/modules/apiweb/web_track_request.js b/api_server/modules/apiweb/web_track_request.js
index 71a809a52..13f19b579 100644
--- a/api_server/modules/apiweb/web_track_request.js
+++ b/api_server/modules/apiweb/web_track_request.js
@@ -3,9 +3,9 @@
const RestfulRequest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/restful_request.js');
const ALLOWED_EVENTS = [
- 'codestream/ide_redirect displayed',
- 'codestream/ide_redirect failed',
- 'codestream/ide selected'
+ 'codestream/ide_redirect displayed',
+ 'codestream/ide_redirect failed',
+ 'codestream/ide selected'
];
class WebTrackRequest extends RestfulRequest {
@@ -24,23 +24,30 @@ class WebTrackRequest extends RestfulRequest {
{
required: {
string: ['event'],
- object:['properties']
+ object:['properties']
},
+ optional: {
+ string: ['nrUserId']
+ }
}
);
- const { event } = this.request.body;
- if (!ALLOWED_EVENTS.includes(event)) {
- throw this.errorHandler.error('invalidParameter', { info: 'not a valid event' });
- }
- }
+ const { event } = this.request.body;
+ if (!ALLOWED_EVENTS.includes(event)) {
+ throw this.errorHandler.error('invalidParameter', { info: 'not a valid event' });
+ }
+ }
async process () {
await this.requireAndAllow();
- const { event, properties } = this.request.body;
- this.api.services.analytics.track(event, properties);
- }
+ const { event, properties, nrUserId } = this.request.body;
+ const options = { request: this };
+ if (nrUserId) {
+ options.nrUserId = nrUserId;
+ }
+ this.api.services.analytics.track(event, properties, options);
+ }
}
module.exports = WebTrackRequest;
diff --git a/api_server/modules/code_errors/test/unfollow_link/tracking_test.js b/api_server/modules/code_errors/test/unfollow_link/tracking_test.js
index 1bfc361f2..6759be57a 100644
--- a/api_server/modules/code_errors/test/unfollow_link/tracking_test.js
+++ b/api_server/modules/code_errors/test/unfollow_link/tracking_test.js
@@ -73,12 +73,14 @@ class TrackingTest extends Aggregation(CodeStreamMessageTest, CommonInit) {
event: 'Notification Change',
messageId: data.messageId || '',
timestamp: data.timestamp || '',
+ anonymousId: data.anonymousId || '',
type: 'track',
properties: {
//user_id: this.currentUser.user.nrUserId,
platform: 'codestream',
path: 'N/A (codestream)',
section: 'N/A (codestream)',
+ session_id: data.properties.session_id || '',
meta_data_15: JSON.stringify(expectedMetaData),
meta_data_14: 'change: code_error_unfollowed',
meta_data_13: 'source_of_change: email_link'
diff --git a/api_server/modules/codemarks/test/unfollow_link/tracking_test.js b/api_server/modules/codemarks/test/unfollow_link/tracking_test.js
index 31a4c782c..20e4627b4 100644
--- a/api_server/modules/codemarks/test/unfollow_link/tracking_test.js
+++ b/api_server/modules/codemarks/test/unfollow_link/tracking_test.js
@@ -73,12 +73,14 @@ class TrackingTest extends Aggregation(CodeStreamMessageTest, CommonInit) {
event: 'Notification Change',
messageId: data.messageId || '',
timestamp: data.timestamp || '',
+ anonymousId: data.anonymousId || '',
type: 'track',
properties: {
//user_id: this.currentUser.user.nrUserId,
platform: 'codestream',
path: 'N/A (codestream)',
section: 'N/A (codestream)',
+ session_id: data.properties.session_id || '',
meta_data_15: JSON.stringify(expectedMetaData),
meta_data_14: 'change: codemark_unfollowed',
meta_data_13: 'source_of_change: email_link'
diff --git a/api_server/modules/entities/entities.js b/api_server/modules/entities/entities.js
new file mode 100644
index 000000000..0c6fbef77
--- /dev/null
+++ b/api_server/modules/entities/entities.js
@@ -0,0 +1,60 @@
+// provide a module to handle requests associated with repos
+
+'use strict';
+
+const Restful = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/restful');
+const Entity = require('./entity');
+const EntityCreator = require('./entity_creator');
+const Errors = require('./errors');
+
+// expose these restful routes
+const ENTITIES_STANDARD_ROUTES = {
+ want: ['get', 'getMany', 'post'],
+ baseRouteName: 'entities',
+ requestClasses: {
+ 'get': require('./get_entity_request'),
+ 'getMany': require('./get_entities_request'),
+ 'post': require('./post_entity_request'),
+ }
+};
+
+// expose additional routes
+const ENTITIES_ADDITIONAL_ROUTES = [
+];
+
+class Entities extends Restful {
+
+ get collectionName () {
+ return 'entities'; // name of the data collection
+ }
+
+ get modelName () {
+ return 'entity'; // name of the data model
+ }
+
+ get creatorClass () {
+ return EntityCreator; // use this class to instantiate entities
+ }
+
+ get modelClass () {
+ return Entity; // use this class for the data model
+ }
+
+ get modelDescription () {
+ return 'A single entity, identified by its GUID';
+ }
+
+ // compile all the routes to expose
+ getRoutes () {
+ let standardRoutes = super.getRoutes(ENTITIES_STANDARD_ROUTES);
+ return [...standardRoutes, ...ENTITIES_ADDITIONAL_ROUTES];
+ }
+
+ describeErrors () {
+ return {
+ 'Entities': Errors
+ };
+ }
+}
+
+module.exports = Entities;
diff --git a/api_server/modules/entities/entity.js b/api_server/modules/entities/entity.js
new file mode 100644
index 000000000..841f13d09
--- /dev/null
+++ b/api_server/modules/entities/entity.js
@@ -0,0 +1,16 @@
+// provides the Entity model for handling New Relic entities
+
+'use strict';
+
+const CodeStreamModel = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/models/codestream_model');
+const CodeStreamModelValidator = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/models/codestream_model_validator');
+const EntityAttributes = require('./entity_attributes');
+
+class Entity extends CodeStreamModel {
+
+ getValidator () {
+ return new CodeStreamModelValidator(EntityAttributes);
+ }
+}
+
+module.exports = Entity;
diff --git a/api_server/modules/entities/entity_attributes.js b/api_server/modules/entities/entity_attributes.js
new file mode 100644
index 000000000..de95578ff
--- /dev/null
+++ b/api_server/modules/entities/entity_attributes.js
@@ -0,0 +1,32 @@
+// attributes for entity documents/models
+
+'use strict';
+
+module.exports = {
+ companyId: {
+ type: 'id',
+ required: true,
+ description: 'ID of the @@#company#company@@ with which the repo is associated'
+ },
+ teamId: {
+ type: 'id',
+ required: true,
+ description: 'ID of the @@#team#team@@ with which the repo is associated'
+ },
+ entityId: {
+ type: 'string',
+ required: true,
+ maxLength: 128,
+ description: 'Entity GUID of the entity'
+ },
+ lastUpdated: {
+ type: 'timestamp',
+ required: true,
+ description: 'Last time the entity was viewed in CodeStream'
+ },
+ lastUserId: {
+ type: 'id',
+ required: true,
+ description: 'ID of the last user to view the entity in CodeStream'
+ }
+};
diff --git a/api_server/modules/entities/entity_creator.js b/api_server/modules/entities/entity_creator.js
new file mode 100644
index 000000000..60d51c884
--- /dev/null
+++ b/api_server/modules/entities/entity_creator.js
@@ -0,0 +1,57 @@
+// this class should be used to create all entity documents in the database
+
+'use strict';
+
+const ModelCreator = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/model_creator');
+const Entity = require('./entity');
+const Errors = require('./errors');
+const Path = require('path');
+
+class EntityCreator extends ModelCreator {
+
+ constructor (options) {
+ super(options);
+ this.errorHandler.add(Errors);
+ }
+
+ get modelClass () {
+ return Entity; // class to use to create an Entity model
+ }
+
+ get collectionName () {
+ return 'entities'; // data collection to use
+ }
+
+ // convenience wrapper
+ async createEntity (attributes) {
+ return await this.createModel(attributes);
+ }
+
+ // get attributes that are required for entity creation, and those that are optional,
+ // along with their types
+ getRequiredAndOptionalAttributes () {
+ return {
+ required: {
+ string: ['entityId', 'teamId', 'companyId']
+ },
+ };
+ }
+
+ // validate attributes for the entity we are creating
+ async validateAttributes () {
+ }
+
+ // right before we save the model...
+ async preSave () {
+ this.attributes.creatorId = this.user.id; // establish creator of the entity as originator of the request
+ this.attributes.lastUserId = this.user.id; // establish this user as the last user to "update" the entity
+ this.attributes.lastUpdated = this.attributes.createdAt = Date.now(); // establish date the entity was last "updated"
+ if (this.request.isForTesting()) { // special for-testing header for easy wiping of test data
+ this.attributes._forTesting = true;
+ }
+ this.createId(); // requisition an ID for the entity
+ await super.preSave(); // proceed with the save...
+ }
+}
+
+module.exports = EntityCreator;
diff --git a/api_server/modules/entities/entity_updater.js b/api_server/modules/entities/entity_updater.js
new file mode 100644
index 000000000..cd4950497
--- /dev/null
+++ b/api_server/modules/entities/entity_updater.js
@@ -0,0 +1,37 @@
+// this class should be used to update entity documents in the database
+
+'use strict';
+
+const ModelUpdater = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/model_updater');
+const Entity = require('./entity');
+
+class EntityUpdater extends ModelUpdater {
+
+ get modelClass () {
+ return Entity; // class to use to create an entity model
+ }
+
+ get collectionName () {
+ return 'entities'; // data collection to use
+ }
+
+ // convenience wrapper
+ async updateEntity (id, attributes) {
+ return await this.updateModel(id, attributes);
+ }
+
+ // get attributes that are allowed, we will ignore all others
+ getAllowedAttributes () {
+ return {
+ string: ['lastUserId'],
+ };
+ }
+
+ // called before the entity is actually saved
+ async preSave () {
+ this.attributes.modifiedAt = this.attributes.lastUpdated = Date.now();
+ await super.preSave(); // base-class preSave
+ }
+}
+
+module.exports = EntityUpdater;
diff --git a/api_server/modules/entities/errors.js b/api_server/modules/entities/errors.js
new file mode 100644
index 000000000..a28110071
--- /dev/null
+++ b/api_server/modules/entities/errors.js
@@ -0,0 +1,6 @@
+// Errors related to the repos module
+
+'use strict';
+
+module.exports = {
+};
diff --git a/api_server/modules/entities/get_entities_request.js b/api_server/modules/entities/get_entities_request.js
new file mode 100644
index 000000000..ca091d2d5
--- /dev/null
+++ b/api_server/modules/entities/get_entities_request.js
@@ -0,0 +1,32 @@
+// handle the 'GET /entities' request, to fetch one or more New Relic entities
+
+'use strict';
+
+const GetManyRequest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/get_many_request');
+const Indexes = require('./indexes');
+
+class GetEntitiesRequest extends GetManyRequest {
+
+ async authorize () {
+ // you have access to the entities if you have access to the team ... teamId is required
+ await this.user.authorizeFromTeamId(this.request.query, this);
+ }
+
+ // build the query to fetch multiple entities from the database
+ buildQuery () {
+ if (!this.request.query.teamId) {
+ return this.errorHandler.error('parameterRequired', { info: 'teamId' });
+ }
+ let query = {
+ teamId: decodeURIComponent(this.request.query.teamId).toLowerCase()
+ };
+ return query;
+ }
+
+ // get options associated with the database query to fetch multiple entities
+ getQueryOptions () {
+ return { hint: Indexes.byTeamId };
+ }
+}
+
+module.exports = GetEntitiesRequest;
diff --git a/api_server/modules/entities/get_entity_request.js b/api_server/modules/entities/get_entity_request.js
new file mode 100644
index 000000000..0be693aa5
--- /dev/null
+++ b/api_server/modules/entities/get_entity_request.js
@@ -0,0 +1,11 @@
+// handle a GET /entities/:id request to fetch a single New Relic entity
+
+'use strict';
+
+const GetRequest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/get_request');
+
+class GetEntityRequest extends GetRequest {
+
+}
+
+module.exports = GetEntityRequest;
diff --git a/api_server/modules/entities/indexes.js b/api_server/modules/entities/indexes.js
new file mode 100644
index 000000000..609b68a28
--- /dev/null
+++ b/api_server/modules/entities/indexes.js
@@ -0,0 +1,13 @@
+// these database indexes are in place for the entities module, all fetch queries
+// must use one of these
+
+'use strict';
+
+module.exports = {
+ byTeamId: {
+ teamId: 1
+ },
+ byEntityId: {
+ entityId: 1
+ }
+};
diff --git a/api_server/modules/entities/module.js b/api_server/modules/entities/module.js
new file mode 100644
index 000000000..7da6293f8
--- /dev/null
+++ b/api_server/modules/entities/module.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = require('./entities.js');
diff --git a/api_server/modules/entities/post_entity_request.js b/api_server/modules/entities/post_entity_request.js
new file mode 100644
index 000000000..a2bc80139
--- /dev/null
+++ b/api_server/modules/entities/post_entity_request.js
@@ -0,0 +1,72 @@
+// handle the POST /entities request to create a new New Relic entity
+
+'use strict';
+
+const PostRequest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/post_request');
+const Indexes = require('./indexes');
+const ModelSaver = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/util/restful/model_saver');
+
+class PostEntityRequest extends PostRequest {
+
+ // authorize the request for the current user
+ async authorize () {
+ // user must be a member of the team
+ let teamId = this.request.body.teamId;
+ if (!teamId) {
+ throw this.errorHandler.error('parameterRequired', { info: 'teamId' });
+ }
+ if (typeof teamId !== 'string') {
+ throw this.errorHandler.error('invalidParameter', { info: 'teamId must be a string' });
+ }
+ teamId = teamId.toLowerCase();
+ if (!await this.user.authorizeTeam(teamId, this)) {
+ throw this.errorHandler.error('createAuth');
+ }
+ this.team = await this.data.teams.getById(teamId);
+ if (!this.team || this.team.get('deactivated')) {
+ throw this.errorHandler.error('notFound'); // shouldn't happen
+ }
+ this.request.body.companyId = this.team.get('companyId');
+ }
+
+ // process the request
+ async process () {
+ // does the entity already exist for this team? if so, just update it
+ const entities = await this.data.entities.getByQuery(
+ {
+ entityId: this.request.body.entityId,
+ deactivated: false
+ },
+ {
+ hint: Indexes.byEntityId
+ }
+ );
+ this.entity = entities.find(entity => entity.get('teamId') === this.team.id);
+ if (this.entity) {
+ const now = Date.now();
+ const op = {
+ $set: {
+ lastUserId: this.user.id,
+ lastUpdated: now,
+ modifiedAt: now
+ }
+ };
+ await new ModelSaver({
+ request: this,
+ collection: this.data.entities,
+ id: this.entity.id
+ }).save(op);
+ } else {
+ return super.process();
+ }
+ }
+
+ async handleResponse () {
+ if (!this.gotError && this.entity) {
+ this.responseData = { entity: this.entity.getSanitizedObject({ request: this }) };
+ }
+ return super.handleResponse();
+ }
+}
+
+module.exports = PostEntityRequest;
diff --git a/api_server/modules/entities/test/entity_test_constants.js b/api_server/modules/entities/test/entity_test_constants.js
new file mode 100644
index 000000000..723300e19
--- /dev/null
+++ b/api_server/modules/entities/test/entity_test_constants.js
@@ -0,0 +1,32 @@
+// test constants for testing the entities module
+
+'use strict';
+
+const EntityAttributes = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/modules/entities/entity_attributes');
+
+// fields expected in all entities
+const EXPECTED_ENTITY_FIELDS = [
+ 'id',
+ 'deactivated',
+ 'createdAt',
+ 'modifiedAt',
+ 'creatorId',
+ 'companyId',
+ 'teamId',
+ 'entityId',
+ 'lastUserId',
+ 'lastUpdated'
+];
+
+const EXPECTED_ENTITY_RESPONSE = {
+ repo: EXPECTED_ENTITY_FIELDS
+};
+
+const UNSANITIZED_ATTRIBUTES = Object.keys(EntityAttributes).filter(attribute => {
+ return EntityAttributes[attribute].serverOnly;
+});
+
+module.exports = {
+ EXPECTED_ENTITY_RESPONSE,
+ UNSANITIZED_ATTRIBUTES
+};
diff --git a/api_server/modules/entities/test/get_entities/acl_test.js b/api_server/modules/entities/test/get_entities/acl_test.js
new file mode 100644
index 000000000..ce9b573a5
--- /dev/null
+++ b/api_server/modules/entities/test/get_entities/acl_test.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const GetEntitiesTest = require('./get_entities_test');
+
+class ACLTest extends GetEntitiesTest {
+
+ constructor (options) {
+ super(options);
+ this.currentUserNotOnTeam = true;
+ this.teamOptions.members = [];
+ }
+
+ get description () {
+ return 'should return an error when trying to fetch entities from a team i\'m not a member of';
+ }
+
+ getExpectedError () {
+ return {
+ code: 'RAPI-1009'
+ };
+ }
+}
+
+module.exports = ACLTest;
diff --git a/api_server/modules/entities/test/get_entities/get_entities_test.js b/api_server/modules/entities/test/get_entities/get_entities_test.js
new file mode 100644
index 000000000..35fb6e2a9
--- /dev/null
+++ b/api_server/modules/entities/test/get_entities/get_entities_test.js
@@ -0,0 +1,79 @@
+'use strict';
+
+const CodeStreamAPITest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/test_base/codestream_api_test');
+const BoundAsync = require(process.env.CSSVC_BACKEND_ROOT + '/shared/server_utils/bound_async');
+const Assert = require('assert');
+
+class GetEntitiesTest extends CodeStreamAPITest {
+
+ constructor (options) {
+ super(options);
+ this.teamOptions.creatorIndex = 1;
+ this.numEntities = 5;
+ }
+
+ get description () {
+ return 'should return New Relic entities when requested';
+ }
+
+ // before the test runs...
+ before (callback) {
+ BoundAsync.series(this, [
+ super.before,
+ this.setPath,
+ this.createEntities
+ ], callback);
+ }
+
+ // set the path to use for the test request
+ setPath (callback) {
+ this.path = `/entities?teamId=${this.team.id}`;
+ callback();
+ }
+
+ // create the entities
+ createEntities (callback) {
+ this.expectedResponse = { entities: [] };
+ BoundAsync.timesSeries(
+ this,
+ this.numEntities,
+ this.createEntity,
+ callback
+ );
+ }
+
+ createEntity (n, callback) {
+ const entityData = this.entityFactory.getRandomEntityData();
+ entityData.teamId = this.team.id;
+ const token = (this.currentUserNotOnTeam || (n % 2)) ? this.users[1].accessToken : this.currentUser.accessToken;
+ this.doApiRequest(
+ {
+ method: 'post',
+ path: '/entities',
+ data: entityData,
+ token
+ },
+ (error, response) => {
+ if (error) { return callback(error); }
+ this.expectedResponse.entities.push(response.entity);
+ callback();
+ }
+ );
+ }
+
+ // validate the request response
+ validateResponse (data) {
+ // sort both arrays by id, to make sure they match
+ this.expectedResponse.entities.sort((a, b) => {
+ return b.id - a.id;
+ });
+ data.entities.sort((a, b) => {
+ return b.id - a.id;
+ });
+
+ // response should exactly equal the response we got when we created the entities
+ Assert.deepStrictEqual(data, this.expectedResponse, 'incorrect response');
+ }
+}
+
+module.exports = GetEntitiesTest;
diff --git a/api_server/modules/entities/test/get_entities/team_id_required_test.js b/api_server/modules/entities/test/get_entities/team_id_required_test.js
new file mode 100644
index 000000000..a68714d3d
--- /dev/null
+++ b/api_server/modules/entities/test/get_entities/team_id_required_test.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const GetEntitiesTest = require('./get_entities_test');
+
+class TeamIDRequiredTest extends GetEntitiesTest {
+
+ get description () {
+ return 'should return an error if teamId is not provided to a query for entities';
+ }
+
+ getExpectedError () {
+ return {
+ code: 'RAPI-1001',
+ info: 'teamId'
+ };
+ }
+
+ setPath (callback) {
+ this.path = '/entities'; // no teamId
+ callback();
+ }
+}
+
+module.exports = TeamIDRequiredTest;
diff --git a/api_server/modules/entities/test/get_entities/test.js b/api_server/modules/entities/test/get_entities/test.js
new file mode 100644
index 000000000..97ec6089a
--- /dev/null
+++ b/api_server/modules/entities/test/get_entities/test.js
@@ -0,0 +1,18 @@
+// handle unit tests for the "GET /entities" request
+
+'use strict';
+
+const GetEntitiesTest = require('./get_entities_test');
+const TeamIdRequiredTest = require('./team_id_required_test');
+const ACLTest = require('./acl_test');
+
+class GetEntitiesRequestTester {
+
+ test () {
+ new GetEntitiesTest().test();
+ new TeamIdRequiredTest().test();
+ new ACLTest().test();
+ }
+}
+
+module.exports = new GetEntitiesRequestTester();
diff --git a/api_server/modules/entities/test/get_entity/acl_test.js b/api_server/modules/entities/test/get_entity/acl_test.js
new file mode 100644
index 000000000..a1c8e8a3a
--- /dev/null
+++ b/api_server/modules/entities/test/get_entity/acl_test.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const GetEntityTest = require('./get_entity_test');
+
+class ACLTest extends GetEntityTest {
+
+ constructor (options) {
+ super(options);
+ this.teamOptions.members = [];
+ this.teamCreatorCreatesEntity = true;
+ }
+
+ get description () {
+ return 'should return an error when trying to fetch a New Relic entity from a team the current user is not a member of';
+ }
+
+ getExpectedError () {
+ return {
+ code: 'RAPI-1009'
+ };
+ }
+}
+
+module.exports = ACLTest;
diff --git a/api_server/modules/entities/test/get_entity/get_entity_test.js b/api_server/modules/entities/test/get_entity/get_entity_test.js
new file mode 100644
index 000000000..5e21d7f91
--- /dev/null
+++ b/api_server/modules/entities/test/get_entity/get_entity_test.js
@@ -0,0 +1,67 @@
+'use strict';
+
+const CodeStreamAPITest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/test_base/codestream_api_test');
+const BoundAsync = require(process.env.CSSVC_BACKEND_ROOT + '/shared/server_utils/bound_async');
+const EntityTestConstants = require('../entity_test_constants');
+
+class GetEntityTest extends CodeStreamAPITest {
+
+ constructor (options) {
+ super(options);
+ this.teamOptions.creatorIndex = 1;
+ }
+
+ get description () {
+ return 'should return a valid entity when requesting an entity created by me';
+ }
+
+ getExpectedFields () {
+ return { entity: EntityTestConstants.EXPECTED_ENTITY_FIELDS };
+ }
+
+ // before the test runs...
+ before (callback) {
+ BoundAsync.series(this, [
+ super.before,
+ this.createEntity,
+ this.setPath
+ ], callback);
+ }
+
+ // create an entity to fetch
+ createEntity (callback) {
+ const entityData = this.entityFactory.getRandomEntityData();
+ entityData.teamId = this.team.id;
+ const token = this.teamCreatorCreatesEntity ? this.users[1].accessToken : this.currentUser.accessToken;
+ this.doApiRequest(
+ {
+ method: 'post',
+ path: '/entities',
+ data: entityData,
+ token
+ },
+ (error, response) => {
+ if (error) { return callback(error); }
+ this.entity = response.entity;
+ callback();
+ }
+ );
+ }
+
+ // set the path for the test request
+ setPath (callback) {
+ // fetch the entity created
+ this.path = '/entities/' + this.entity.id;
+ callback();
+ }
+
+ // validate the response to the test request
+ validateResponse (data) {
+ // make sure we got the expected entity
+ this.validateMatchingObject(this.entity.id, data.entity, 'entity');
+ // make sure we didn't get attributes not suitable for the client
+ this.validateSanitized(data.entity, EntityTestConstants.UNSANITIZED_ATTRIBUTES);
+ }
+}
+
+module.exports = GetEntityTest;
diff --git a/api_server/modules/entities/test/get_entity/get_other_entity_test.js b/api_server/modules/entities/test/get_entity/get_other_entity_test.js
new file mode 100644
index 000000000..d13990c06
--- /dev/null
+++ b/api_server/modules/entities/test/get_entity/get_other_entity_test.js
@@ -0,0 +1,17 @@
+'use strict';
+
+const GetEntityTest = require('./get_entity_test');
+
+class GetOtherEntityTest extends GetEntityTest {
+
+ constructor (options) {
+ super(options);
+ this.teamCreatorCreatesEntity = true;
+ }
+
+ get description () {
+ return 'should return a valid entity when requesting an entity created by another user on a team that i am on';
+ }
+}
+
+module.exports = GetOtherEntityTest;
diff --git a/api_server/modules/entities/test/get_entity/not_found_test.js b/api_server/modules/entities/test/get_entity/not_found_test.js
new file mode 100644
index 000000000..7e95ea159
--- /dev/null
+++ b/api_server/modules/entities/test/get_entity/not_found_test.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const GetEntityTest = require('./get_entity_test');
+const ObjectId = require('mongodb').ObjectId;
+
+class NotFoundTest extends GetEntityTest {
+
+ get description () {
+ return 'should return an error when trying to fetch a New Relic entity that doesn\'t exist';
+ }
+
+ getExpectedError () {
+ return {
+ code: 'RAPI-1003'
+ };
+ }
+
+ // before the test runs...
+ before (callback) {
+ super.before (error => {
+ // substitute a non-existent entity ID
+ if (error) { return callback(error); }
+ this.path = '/entities/' + ObjectId();
+ callback();
+ });
+ }
+}
+
+module.exports = NotFoundTest;
diff --git a/api_server/modules/entities/test/get_entity/test.js b/api_server/modules/entities/test/get_entity/test.js
new file mode 100644
index 000000000..2751d18d5
--- /dev/null
+++ b/api_server/modules/entities/test/get_entity/test.js
@@ -0,0 +1,21 @@
+// handle unit tests for the "GET /entities/:id" request,
+// to fetch a New Relic entity
+
+'use strict';
+
+const GetEntityTest = require('./get_entity_test');
+const NotFoundTest = require('./not_found_test');
+const ACLTest = require('./acl_test');
+const GetOtherEntityTest = require('./get_other_entity_test');
+
+class GetEntityRequestTester {
+
+ test () {
+ new GetEntityTest().test();
+ new NotFoundTest().test();
+ new ACLTest().test();
+ new GetOtherEntityTest().test();
+ }
+}
+
+module.exports = new GetEntityRequestTester();
diff --git a/api_server/modules/entities/test/post_entity/acl_test.js b/api_server/modules/entities/test/post_entity/acl_test.js
new file mode 100644
index 000000000..4772b8c97
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/acl_test.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const PostEntityTest = require('./post_entity_test');
+
+class ACLTest extends PostEntityTest {
+
+ setTestOptions (callback) {
+ super.setTestOptions(() => {
+ this.teamOptions.members = [];
+ callback();
+ });
+ }
+
+ get description () {
+ return 'should return an error when trying to create a New Relic entity in a team the current user is not a member of';
+ }
+
+ getExpectedError () {
+ return {
+ code: 'RAPI-1011'
+ };
+ }
+}
+
+module.exports = ACLTest;
diff --git a/api_server/modules/entities/test/post_entity/common_init.js b/api_server/modules/entities/test/post_entity/common_init.js
new file mode 100644
index 000000000..e3ca570ef
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/common_init.js
@@ -0,0 +1,59 @@
+// base class for many tests of the "POST /entities" requests
+
+'use strict';
+
+const BoundAsync = require(process.env.CSSVC_BACKEND_ROOT + '/shared/server_utils/bound_async');
+const CodeStreamAPITest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/test_base/codestream_api_test');
+
+class CommonInit {
+
+ init (callback) {
+ BoundAsync.series(this, [
+ this.setTestOptions,
+ CodeStreamAPITest.prototype.before.bind(this),
+ this.setPath,
+ this.makeEntityData // make the data to be used during the request
+ ], callback);
+ }
+
+ setTestOptions (callback) {
+ this.entityOptions = {};
+ this.teamOptions.creatorIndex = 1;
+ callback();
+ }
+
+ // form the data for the generating the entity
+ makeEntityData (callback) {
+ this.data = this.entityFactory.getRandomEntityData(this.entityOptions);
+ this.data.teamId = this.team.id;
+ this.createdAfter = Date.now();
+ callback();
+ }
+
+ // set the path to use during the test request
+ setPath (callback) {
+ this.path = '/entities';
+ callback();
+ }
+
+ // create the entity for real
+ createEntity (callback) {
+ const token = this.useToken || (this.otherUserCreatesEntity ? this.users[1].accessToken : this.currentUser.accessToken);
+ this.doApiRequest(
+ {
+ method: 'post',
+ path: `/entities`,
+ data: this.data,
+ token
+ },
+ (error, response) => {
+ if (error) { return callback(error); }
+ this.entityResponse = response;
+ delete this.data; // don't need this anymore
+ callback();
+ }
+ );
+ }
+}
+
+module.exports = CommonInit;
diff --git a/api_server/modules/entities/test/post_entity/deactivated_team_test.js b/api_server/modules/entities/test/post_entity/deactivated_team_test.js
new file mode 100644
index 000000000..b78e1dd9b
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/deactivated_team_test.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const PostEntityTest = require('./post_entity_test');
+
+class DeactivatedTeamTest extends PostEntityTest {
+
+ get description () {
+ return 'should return an error when trying to create a New Relic entity in a deactivated team';
+ }
+
+ getExpectedError () {
+ return {
+ code: 'RAPI-1003'
+ };
+ }
+
+ before (callback) {
+ super.before(error => {
+ if (error) { return callback(error); }
+ this.doApiRequest({
+ method: 'delete',
+ path: '/teams/' + this.team.id,
+ token: this.currentUser.accessToken,
+ requestOptions: {
+ headers: {
+ 'X-Delete-Team-Secret': this.apiConfig.sharedSecrets.confirmationCheat
+ }
+ }
+ }, callback);
+ });
+ }
+}
+
+module.exports = DeactivatedTeamTest;
diff --git a/api_server/modules/entities/test/post_entity/entity_exists_other_team_test.js b/api_server/modules/entities/test/post_entity/entity_exists_other_team_test.js
new file mode 100644
index 000000000..9e55ec9e3
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/entity_exists_other_team_test.js
@@ -0,0 +1,58 @@
+'use strict';
+
+const PostEntityTest = require('./post_entity_test');
+const Assert = require('assert');
+const BoundAsync = require(process.env.CSSVC_BACKEND_ROOT + '/shared/server_utils/bound_async');
+
+class EntityExistsOtherTeamTest extends PostEntityTest {
+
+ constructor (options) {
+ super(options);
+ this.otherUserCreatesEntity = true;
+ }
+
+ get description () {
+ return 'when creating a New Relic entity, if the entity already exists but for another team, the entity will be created for this team';
+ }
+
+ before (callback) {
+ BoundAsync.series(this, [
+ super.before,
+ this.createOtherTeam,
+ this.createEntity,
+ this.restoreData
+ ], callback);
+ }
+
+ // create another team that the current user is not on
+ createOtherTeam (callback) {
+ this.companyFactory.createRandomCompany(
+ (error, response) => {
+ if (error) { return callback(error); }
+ this.data.teamId = response.team.id;
+ this.useToken = response.accessToken;
+ callback();
+ },
+ {
+ token: this.users[1].accessToken
+ }
+ );
+ }
+
+ // restore data to be used in the test request after munging it
+ restoreData (callback) {
+ this.data = {
+ entityId: this.entityResponse.entity.entityId,
+ teamId: this.team.id
+ };
+ this.otherUserCreatesEntity = false;
+ callback();
+ }
+
+ validateResponse (data) {
+ Assert.notStrictEqual(data.entity.id, this.entityResponse.entity.id, 'entity created by test request is equal to the entity created on the other team');
+ return super.validateResponse(data);
+ }
+}
+
+module.exports = EntityExistsOtherTeamTest;
diff --git a/api_server/modules/entities/test/post_entity/entity_exists_other_user_test.js b/api_server/modules/entities/test/post_entity/entity_exists_other_user_test.js
new file mode 100644
index 000000000..30de6b0f2
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/entity_exists_other_user_test.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const EntityExistsTest = require('./entity_exists_test');
+const Assert = require('assert');
+
+class EntityExistsOtherUserTest extends EntityExistsTest {
+
+ constructor (options) {
+ super(options);
+ this.otherUserCreatesEntity = true;
+ }
+
+ get description () {
+ return 'when creating a New Relic entity, if the entity already exists for the team, the existing entity should be updated, and the user ID should be updated to the user making the request';
+ }
+
+ validateResponse (data) {
+ Assert.strictEqual(this.entityResponse.entity.lastUserId, this.users[1].user.id, 'entity originally created does not have id of entity creator for creatorId');
+ return super.validateResponse(data);
+ }
+}
+
+module.exports = EntityExistsOtherUserTest;
diff --git a/api_server/modules/entities/test/post_entity/entity_exists_test.js b/api_server/modules/entities/test/post_entity/entity_exists_test.js
new file mode 100644
index 000000000..6ffde4ec1
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/entity_exists_test.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const PostEntityTest = require('./post_entity_test');
+const Assert = require('assert');
+
+class EntityExistsTest extends PostEntityTest {
+
+ get description () {
+ return 'when creating a New Relic entity, if the entity already exists for the team, the existing entity should be updated';
+ }
+
+ before (callback) {
+ // create the entity before making the test request
+ this.expectedVersion = 2;
+ super.before(error => {
+ if (error) { return callback(error); }
+ this.savedData = this.data; // the base test deletes this.data, but we need it for the second try
+ setTimeout(() => { // give a little time for our timestamps to change
+ this.createEntity(error => {
+ if (error) { return callback(error); }
+ this.updatedAfter = Date.now();
+ this.data = this.savedData;
+ callback();
+ });
+ }, 2);
+ });
+ }
+
+ validateResponse (data) {
+ Assert.strictEqual(data.entity.id, this.entityResponse.entity.id, 'id of returned entity does not match the previously created entity');
+ return super.validateResponse(data);
+ }
+}
+
+module.exports = EntityExistsTest;
diff --git a/api_server/modules/entities/test/post_entity/fetch_test.js b/api_server/modules/entities/test/post_entity/fetch_test.js
new file mode 100644
index 000000000..790dd5e12
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/fetch_test.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const PostEntityTest = require('./post_entity_test');
+const BoundAsync = require(process.env.CSSVC_BACKEND_ROOT + '/shared/server_utils/bound_async');
+const Assert = require('assert');
+
+class FetchTest extends PostEntityTest {
+
+ get description () {
+ return 'should persist an entity when creating a New Relic entity, checked by fetching the entity';
+ }
+
+ get method () {
+ return 'get';
+ }
+
+ // before the test runs...
+ before (callback) {
+ BoundAsync.series(this, [
+ super.before,
+ this.createEntity,
+ this.setPath
+ ], callback);
+ }
+
+ setPath (callback) {
+ if (!this.entityResponse) { return callback(); }
+ this.path = `/entities/${this.entityResponse.entity.id}`;
+ callback();
+ }
+
+ // validate that the response is correct
+ validateResponse (data) {
+ Assert.strictEqual(data.entity.id, this.entityResponse.entity.id, 'fetched entity not equal to the entity given in the response');
+ Assert.strictEqual(data.entity.entityId, this.entityResponse.entity.entityId, 'entityId of fetched entity does not match the entityId sent');
+ }
+}
+
+module.exports = FetchTest;
diff --git a/api_server/modules/entities/test/post_entity/invalid_parameter_test.js b/api_server/modules/entities/test/post_entity/invalid_parameter_test.js
new file mode 100644
index 000000000..c4bc44244
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/invalid_parameter_test.js
@@ -0,0 +1,28 @@
+'use strict';
+
+const PostEntityTest = require('./post_entity_test');
+
+class InvalidParameterTest extends PostEntityTest {
+
+ get description () {
+ return `should return an error when trying to create a New Relic entity with an invalid ${this.parameter}`;
+ }
+
+ getExpectedError () {
+ return {
+ code: 'RAPI-1012',
+ info: this.parameter
+ };
+ }
+
+ // before the test runs...
+ before (callback) {
+ super.before(error => {
+ if (error) { return callback(error); }
+ this.data[this.parameter] = Math.floor(Math.random() * 100000000);
+ callback();
+ });
+ }
+}
+
+module.exports = InvalidParameterTest;
diff --git a/api_server/modules/entities/test/post_entity/parameter_required_test.js b/api_server/modules/entities/test/post_entity/parameter_required_test.js
new file mode 100644
index 000000000..7f1841fbc
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/parameter_required_test.js
@@ -0,0 +1,28 @@
+'use strict';
+
+const PostEntityTest = require('./post_entity_test');
+
+class ParameterRequiredTest extends PostEntityTest {
+
+ get description () {
+ return `should return an error when trying to create a New Relic entity with no ${this.parameter}`;
+ }
+
+ getExpectedError () {
+ return {
+ code: 'RAPI-1001',
+ info: this.parameter
+ };
+ }
+
+ // before the test runs...
+ before (callback) {
+ super.before(error => {
+ if (error) { return callback(error); }
+ delete this.data[this.parameter];
+ callback();
+ });
+ }
+}
+
+module.exports = ParameterRequiredTest;
diff --git a/api_server/modules/entities/test/post_entity/post_entity_test.js b/api_server/modules/entities/test/post_entity/post_entity_test.js
new file mode 100644
index 000000000..5a5b71ecc
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/post_entity_test.js
@@ -0,0 +1,65 @@
+// base class for many tests of the "POST /entities" requests
+
+'use strict';
+
+const Aggregation = require(process.env.CSSVC_BACKEND_ROOT + '/shared/server_utils/aggregation');
+const Assert = require('assert');
+const CodeStreamAPITest = require(process.env.CSSVC_BACKEND_ROOT + '/api_server/lib/test_base/codestream_api_test');
+const EntityTestConstants = require('../entity_test_constants');
+const CommonInit = require('./common_init');
+
+class PostEntityTest extends Aggregation(CodeStreamAPITest, CommonInit) {
+
+ constructor (options) {
+ super(options);
+ }
+
+ get description () {
+ return 'should return a valid entity when creating a New Relic entity';
+ }
+
+ get method () {
+ return 'post';
+ }
+
+ getExpectedFields () {
+ return { entity: EntityTestConstants.EXPECTED_ENTITY_FIELDS };
+ }
+
+ // before the test runs...
+ before (callback) {
+ this.init(callback);
+ }
+
+ // validate the response to the test request
+ validateResponse (data) {
+ // verify we got back an entity with the attributes we specified
+ const { entity } = data;
+ const errors = [];
+ const lastUpdated = this.updatedAfter ? entity.modifiedAt : entity.createdAt;
+ const updatedAfter = this.updatedAfter || this.createdAfter;
+ const creatorId = this.otherUserCreatesEntity ? this.users[1].user.id : this.currentUser.user.id;
+ const expectedVersion = this.expectedVersion || 1;
+ const result = (
+ ((entity.id === entity._id) || errors.push('id not set to _id')) && // DEPRECATE ME
+ ((entity.entityId === this.data.entityId) || errors.push('entity does not match')) &&
+ ((entity.teamId === this.team.id) || errors.push('teamId does not match the test team')) &&
+ ((entity.companyId === this.company.id) || errors.push('companyId does not match the test company')) &&
+ ((entity.deactivated === false) || errors.push('deactivated not false')) &&
+ ((typeof entity.createdAt === 'number') || errors.push('createdAt is not a number')) &&
+ ((entity.createdAt >= this.createdAfter) || errors.push('createdAt not greater than or equal to when the entity should have been created')) &&
+ ((typeof entity.lastUpdated === 'number') || errors.push('lastUpdated is not a number')) &&
+ ((entity.modifiedAt >= updatedAfter) || errors.push('modifiedAt not greater than or equal to after the entity should have been updated')) &&
+ ((entity.lastUpdated === lastUpdated) || errors.push('lastUpdated not equal to expected value')) &&
+ ((entity.creatorId === creatorId) || errors.push('creatorId not equal to expected creator')) &&
+ ((entity.lastUserId === this.currentUser.user.id) || errors.push('lastUserId not equal to current user id')) &&
+ ((entity.version === expectedVersion) || errors.push('version is incorrect'))
+ );
+ Assert(result === true && errors.length === 0, 'response not valid: ' + errors.join(', '));
+
+ // verify the entity in the response has no attributes that should not go to clients
+ this.validateSanitized(entity, EntityTestConstants.UNSANITIZED_ATTRIBUTES);
+ }
+}
+
+module.exports = PostEntityTest;
diff --git a/api_server/modules/entities/test/post_entity/test.js b/api_server/modules/entities/test/post_entity/test.js
new file mode 100644
index 000000000..39b4e0db9
--- /dev/null
+++ b/api_server/modules/entities/test/post_entity/test.js
@@ -0,0 +1,33 @@
+// handle unit tests for the "POST /entities" request,
+// to create a New Relic entity
+
+'use strict';
+
+const PostEntityTest = require('./post_entity_test');
+const ParameterRequiredTest = require('./parameter_required_test');
+const InvalidParameterTest = require('./invalid_parameter_test');
+const ACLTest = require('./acl_test');
+const DeactivatedTeamTest = require('./deactivated_team_test');
+const FetchTest = require('./fetch_test');
+const EntityExistsTest = require('./entity_exists_test');
+const EntityExistsOtherUserTest = require('./entity_exists_other_user_test');
+const EntityExistsOtherTeamTest = require('./entity_exists_other_team_test');
+
+class PostEntityRequestTester {
+
+ test () {
+ new PostEntityTest().test();
+ new ParameterRequiredTest({ parameter: 'teamId' }).test();
+ new ParameterRequiredTest({ parameter: 'entityId' }).test();
+ new InvalidParameterTest({ parameter: 'teamId' }).test();
+ new InvalidParameterTest({ parameter: 'entityId' }).test();
+ new ACLTest().test();
+ new DeactivatedTeamTest().test();
+ new FetchTest().test();
+ new EntityExistsTest().test();
+ new EntityExistsOtherUserTest().test();
+ new EntityExistsOtherTeamTest().test();
+ }
+}
+
+module.exports = new PostEntityRequestTester();
diff --git a/api_server/modules/entities/test/random_entity_factory.js b/api_server/modules/entities/test/random_entity_factory.js
new file mode 100644
index 000000000..2a47fa2c5
--- /dev/null
+++ b/api_server/modules/entities/test/random_entity_factory.js
@@ -0,0 +1,43 @@
+// provide a factory for creating random repos, for testing purposes
+
+'use strict';
+
+const RandomString = require('randomstring');
+
+class RandomEntityFactory {
+
+ constructor (options) {
+ Object.assign(this, options);
+ }
+
+ // create the entity by submitting a request to the server
+ createEntity (data, token, callback) {
+ this.apiRequester.doApiRequest({
+ method: 'post',
+ path: '/entities',
+ data: data,
+ token: token
+ }, callback);
+ }
+
+ // generate a random entity guid
+ randomEntityGuid (options = {}) {
+ return RandomString.generate(79);
+ }
+
+ // get some random attributes to create a random entity
+ getRandomEntityData (options = {}) {
+ let data = {
+ entityId: this.randomEntityGuid(options)
+ };
+ return data;
+ }
+
+ // create a random entity in the database
+ createRandomEntity (callback, options = {}) {
+ const data = this.getRandomEntityData(options);
+ this.createEntity(data, options.token, callback);
+ }
+}
+
+module.exports = RandomEntityFactory;
diff --git a/api_server/modules/entities/test/test.js b/api_server/modules/entities/test/test.js
new file mode 100644
index 000000000..f86dcd3b3
--- /dev/null
+++ b/api_server/modules/entities/test/test.js
@@ -0,0 +1,17 @@
+// handle unit tests for the entities module
+
+'use strict';
+
+// make eslint happy
+/* globals describe */
+
+const GetEntityRequestTester = require('./get_entity/test');
+const GetEntitiesRequestTester = require('./get_entities/test');
+const PostEntityRequestTester = require('./post_entity/test');
+
+describe('entity requests', function() {
+
+ describe('GET /entities/:id', GetEntityRequestTester.test);
+ describe('GET /entities', GetEntitiesRequestTester.test);
+ describe('POST /entities', PostEntityRequestTester.test);
+});
diff --git a/api_server/modules/inbound_emails/test/tracking_test.js b/api_server/modules/inbound_emails/test/tracking_test.js
index 4fd965e15..68b10df4e 100644
--- a/api_server/modules/inbound_emails/test/tracking_test.js
+++ b/api_server/modules/inbound_emails/test/tracking_test.js
@@ -106,12 +106,14 @@ class TrackingTest extends InboundEmailMessageTest {
event: 'Reply Created',
messageId: data.messageId || '',
timestamp: data.timestamp || '',
+ anonymousId: data.anonymousId || '',
type: 'track',
properties: {
//user_id: this.currentUser.user.nrUserId,
platform: 'codestream',
path: 'N/A (codestream)',
section: 'N/A (codestream)',
+ session_id: data.properties.session_id || '',
meta_data_15: JSON.stringify(expectedMetaData),
'Parent ID': parentId,
'Parent Type': parentType,
diff --git a/api_server/modules/reviews/test/unfollow_link/tracking_test.js b/api_server/modules/reviews/test/unfollow_link/tracking_test.js
index 5b6d3c325..743d9de23 100644
--- a/api_server/modules/reviews/test/unfollow_link/tracking_test.js
+++ b/api_server/modules/reviews/test/unfollow_link/tracking_test.js
@@ -73,12 +73,14 @@ class TrackingTest extends Aggregation(CodeStreamMessageTest, CommonInit) {
event: 'Notification Change',
messageId: data.messageId || '',
timestamp: data.timestamp || '',
+ anonymousId: data.anonymousId || '',
type: 'track',
properties: {
//user_id: this.currentUser.user.nrUserId,
platform: 'codestream',
path: 'N/A (codestream)',
section: 'N/A (codestream)',
+ session_id: data.properties.session_id || '',
meta_data_15: JSON.stringify(expectedMetaData),
meta_data_14: 'change: review_unfollowed',
meta_data_13: 'source_of_change: email_link'
diff --git a/api_server/modules/test.js b/api_server/modules/test.js
index c7bd82a47..fa7150229 100644
--- a/api_server/modules/test.js
+++ b/api_server/modules/test.js
@@ -27,4 +27,5 @@ describe('modules', () => {
require('./marker_locations/test/test.js');
require('./newrelic_comments/test/test.js');
require('./environment_manager/test/test.js');
+ require('./entities/test/test.js');
});
diff --git a/api_server/modules/users/test/unsubscribe_notification/tracking_test.js b/api_server/modules/users/test/unsubscribe_notification/tracking_test.js
index b02d5df4b..a73bcb832 100644
--- a/api_server/modules/users/test/unsubscribe_notification/tracking_test.js
+++ b/api_server/modules/users/test/unsubscribe_notification/tracking_test.js
@@ -64,12 +64,14 @@ class TrackingTest extends Aggregation(CodeStreamMessageTest, CommonInit) {
event: 'codestream/email_unsubscribe succeeded',
messageId: data.messageId || '',
timestamp: data.timestamp || '',
+ anonymousId: data.anonymousId || '',
type: 'track',
properties: {
//user_id: this.currentUser.user.nrUserId,
platform: 'codestream',
path: 'N/A (codestream)',
section: 'N/A (codestream)',
+ session_id: data.properties.session_id || '',
meta_data_15: JSON.stringify(expectedMetaData),
'meta_data': 'email_type: discussion',
'event_type': 'response'
diff --git a/api_server/modules/users/test/unsubscribe_reminder/tracking_test.js b/api_server/modules/users/test/unsubscribe_reminder/tracking_test.js
index 8f9de8bc2..d1fa624f0 100644
--- a/api_server/modules/users/test/unsubscribe_reminder/tracking_test.js
+++ b/api_server/modules/users/test/unsubscribe_reminder/tracking_test.js
@@ -64,12 +64,14 @@ class TrackingTest extends Aggregation(CodeStreamMessageTest, CommonInit) {
event: 'codestream/email_unsubscribe succeeded',
messageId: data.messageId || '',
timestamp: data.timestamp || '',
+ anonymousId: data.anonymousId || '',
type: 'track',
properties: {
//user_id: this.currentUser.user.nrUserId,
platform: 'codestream',
path: 'N/A (codestream)',
section: 'N/A (codestream)',
+ session_id: data.properties.session_id || '',
meta_data_15: JSON.stringify(expectedMetaData),
'meta_data': 'email_type: reminder',
'event_type': 'response'
diff --git a/api_server/modules/users/test/unsubscribe_weekly/tracking_test.js b/api_server/modules/users/test/unsubscribe_weekly/tracking_test.js
index c33d94514..c48e2b3f8 100644
--- a/api_server/modules/users/test/unsubscribe_weekly/tracking_test.js
+++ b/api_server/modules/users/test/unsubscribe_weekly/tracking_test.js
@@ -68,12 +68,14 @@ class TrackingTest extends Aggregation(CodeStreamMessageTest, CommonInit) {
event: 'codestream/email_unsubscribe succeeded',
messageId: data.messageId || '',
timestamp: data.timestamp || '',
+ anonymousId: data.anonymousId || '',
type: 'track',
properties: {
//user_id: this.currentUser.user.nrUserId,
platform: 'codestream',
path: 'N/A (codestream)',
section: 'N/A (codestream)',
+ session_id: data.properties.session_id || '',
meta_data_15: JSON.stringify(expectedMetaData),
'meta_data': 'email_type: weekly_activity',
'event_type': 'response'
diff --git a/api_server/modules/users/user.js b/api_server/modules/users/user.js
index ff41431d6..ea972d408 100644
--- a/api_server/modules/users/user.js
+++ b/api_server/modules/users/user.js
@@ -82,6 +82,8 @@ class User extends CodeStreamModel {
return await this.authorizeReview(id, request, options);
case 'codeError':
return await this.authorizeCodeError(id, request, options);
+ case 'entity':
+ return await this.authorizeEntity(id, request, options);
case 'user':
return await this.authorizeUser(id, request, options);
default:
@@ -240,6 +242,17 @@ class User extends CodeStreamModel {
return authorized ? codeError : false;
}
+ // authorize the user to "access" a New Relic entity, based on ID
+ async authorizeEntity (id, request, options) {
+ const entity = await request.data.entities.getById(id, options);
+ if (!entity) {
+ throw request.errorHandler.error('notFound', { info: 'entity' });
+ }
+ // to access an entity, the user must be on the team that owns it
+ const authorized = entity.get('teamId') && this.hasTeam(entity.get('teamId'));
+ return authorized ? entity : false;
+ }
+
// authorize the user to "access" a user model, based on ID
async authorizeUser (id, request) {
// user can always access their own me-object
diff --git a/api_server/modules/web/styles/web.css b/api_server/modules/web/styles/web.css
index d6b1d89d9..b6024021d 100644
--- a/api_server/modules/web/styles/web.css
+++ b/api_server/modules/web/styles/web.css
@@ -192,6 +192,42 @@ h6 {
color: white!important;
text-decoration: underline;
}
+.content-body-container {
+ max-width: 80%;
+ margin: 0 auto;
+ text-align: left;
+}
+.content-body-section-header {
+ margin-top: 28px;
+}
+.content-body-header {
+ font-size: xx-large;
+}
+.content-body-tab-container {
+ margin-top: 32px;
+ margin-bottom: 10px;
+ border-bottom: 1px solid #3c3c3c;
+ display: flex;
+ justify-content: space-between;
+}
+.content-body-tab-header {
+ font-size: larger;
+ cursor: pointer;
+ display: inline-block;
+ text-align: center;
+ padding-bottom: 5px;
+ margin-bottom: -1px;
+}
+.content-body-copy {
+ margin-bottom: 12px;
+}
+.content-body-tab {
+ display: none;
+}
+.content-active-tab {
+ /* display: block !important; */
+ border-bottom: 1px solid;
+}
.box-border {
margin-top: 1rem;
padding: 1.5rem;
@@ -550,7 +586,7 @@ hr {
.interstitial-detail-wrapper {
padding: 30px;
- margin: 0 0 40px 0;
+ margin: 10px 0 10px 0;
max-width: 704px;
margin-right: auto;
margin-left: auto;
@@ -637,8 +673,21 @@ hr {
.observability-gif {
width: 100%;
+ border-radius: 8px;
+ margin-top: 10px;
}
+@media (max-width: 768px) {
+ .content-body-tab-icon {
+ display: none !important;
+ }
+ .content-body-tab-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
@media (min-width: 1px) {
.btn-block-wrap {
width: 100%;
@@ -767,11 +816,11 @@ span.dropdown-item:hover {
}
.navbar-light.bg-light {
background: #fff !important;
- margin: 0 0 80px 0;
+ margin: 0 0 0 0;
}
.navbar-dark.bg-dark {
/* background: #1D252C !important; */
- margin: 0 0 80px 0;
+ margin: 0 0 0 0;
border-bottom: 1px solid #2a3036;
padding: 15px 0 15px 0;
}
@@ -796,10 +845,11 @@ span.dropdown-item:hover {
}
.form-control {
border: 1px solid #383838 !important;
+ padding: 6px 0 0 0 !important;
}
.btn-light.form-control {
border: 1px solid rgb(227, 228, 228) !important;
- height: 50px;
+ /* height: 50px; */
}
.lines-added {
color: #66aa66;
diff --git a/api_server/package-lock.json b/api_server/package-lock.json
index 35126c983..1e09c0822 100644
--- a/api_server/package-lock.json
+++ b/api_server/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "api-server",
- "version": "15.3.0",
+ "version": "15.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "api-server",
- "version": "15.3.0",
+ "version": "15.4.0",
"dependencies": {
"@microsoft/microsoft-graph-client": "3.0.7",
"@octokit/rest": "18.12.0",
@@ -61,7 +61,7 @@
"strftime": "0.10.2",
"stripe": "8.217.0",
"toposort": "2.0.2",
- "undici": "5.28.3",
+ "undici": "5.28.4",
"uuid": "8.3.2",
"xregexp": "5.1.1"
},
@@ -11588,9 +11588,9 @@
}
},
"node_modules/tar": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
- "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@@ -11880,9 +11880,9 @@
"dev": true
},
"node_modules/undici": {
- "version": "5.28.3",
- "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz",
- "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==",
+ "version": "5.28.4",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
+ "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
@@ -21541,9 +21541,9 @@
}
},
"tar": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
- "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"requires": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@@ -21772,9 +21772,9 @@
"dev": true
},
"undici": {
- "version": "5.28.3",
- "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz",
- "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==",
+ "version": "5.28.4",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
+ "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"requires": {
"@fastify/busboy": "^2.0.0"
}
diff --git a/api_server/package.json b/api_server/package.json
index d98b89a4d..213457b58 100644
--- a/api_server/package.json
+++ b/api_server/package.json
@@ -55,7 +55,7 @@
"strftime": "0.10.2",
"stripe": "8.217.0",
"toposort": "2.0.2",
- "undici": "5.28.3",
+ "undici": "5.28.4",
"uuid": "8.3.2",
"xregexp": "5.1.1"
},
diff --git a/codestream-docker.json b/codestream-docker.json
index 950940454..d0c666343 100644
--- a/codestream-docker.json
+++ b/codestream-docker.json
@@ -248,7 +248,8 @@
"telemetry": {
"segment": {
"token": "${TELEMETRY_SEGMENT_TOKEN}",
- "webToken": "${TELEMETRY_SEGMENT_WEB_TOKEN}"
+ "webToken": "${TELEMETRY_SEGMENT_WEB_TOKEN}",
+ "telemetryEndpoint": "${TELEMETRY_ENDPOINT}"
}
},
"universalSecrets": {
diff --git a/docker-compose.yml b/docker-compose.yml
index a9368e4da..0c86ce57f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -63,6 +63,8 @@ services:
- INTEGRATIONS_TRELLO_CLOUD_APP_CLIENT_ID
- TELEMETRY_SEGMENT_TOKEN
- TELEMETRY_SEGMENT_WEB_TOKEN
+ - TELEMETRY_ENDPOINT
+ - TELEMETRY_
- UNIVERSAL_SECRETS_TELEMETRY
volumes:
mongodata:
diff --git a/outbound_email/CHANGELOG.md b/outbound_email/CHANGELOG.md
index 5614d76e0..21b7772ed 100644
--- a/outbound_email/CHANGELOG.md
+++ b/outbound_email/CHANGELOG.md
@@ -1,6 +1,6 @@
# Change Log
-## [15.4.0] - 2024-4-4
+## [15.4.0] - 2024-4-17
### Changed