From f32252cdf631378a398d4afbd0d785c53535fe3a Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Wed, 26 Apr 2023 19:55:05 +0100 Subject: [PATCH 001/329] feat(catalog-backend): Add observability for catalog processing Signed-off-by: Mike Bryant Signed-off-by: Mike Bryant --- .changeset/slimy-kids-jam.md | 5 + .../DefaultCatalogProcessingEngine.ts | 320 +++++++++--------- .../DefaultCatalogProcessingOrchestrator.ts | 90 +++-- .../catalog-backend/src/util/opentelemetry.ts | 32 ++ 4 files changed, 266 insertions(+), 181 deletions(-) create mode 100644 .changeset/slimy-kids-jam.md create mode 100644 plugins/catalog-backend/src/util/opentelemetry.ts diff --git a/.changeset/slimy-kids-jam.md b/.changeset/slimy-kids-jam.md new file mode 100644 index 0000000000..ea2897bed1 --- /dev/null +++ b/.changeset/slimy-kids-jam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Added OpenTelemetry spans for catalog processing diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index 8e2f8b2f6f..e9f87c36c7 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -23,7 +23,7 @@ import { assertError, serializeError, stringifyError } from '@backstage/errors'; import { Hash } from 'crypto'; import stableStringify from 'fast-json-stable-stringify'; import { Logger } from 'winston'; -import { metrics } from '@opentelemetry/api'; +import { metrics, SpanStatusCode, trace } from '@opentelemetry/api'; import { ProcessingDatabase, RefreshStateItem } from '../database/types'; import { createCounterMetric, createSummaryMetric } from '../util/metrics'; import { @@ -35,9 +35,12 @@ import { Stitcher } from '../stitching/Stitcher'; import { startTaskPipeline } from './TaskPipeline'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; +import { addEntityAttributes, TRACER_ID } from '../util/opentelemetry'; const CACHE_TTL = 5; +const tracer = trace.getTracer(TRACER_ID); + export type ProgressTracker = ReturnType; export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { @@ -131,177 +134,186 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { } }, processTask: async item => { - const track = this.tracker.processStart(item, this.logger); + await tracer.startActiveSpan('ProcessingRun', async span => { + const track = this.tracker.processStart(item, this.logger); + addEntityAttributes(span, item.entityRef); - try { - const { - id, - state, - unprocessedEntity, - entityRef, - locationKey, - resultHash: previousResultHash, - } = item; - const result = await this.orchestrator.process({ - entity: unprocessedEntity, - state, - }); + try { + const { + id, + state, + unprocessedEntity, + entityRef, + locationKey, + resultHash: previousResultHash, + } = item; + const result = await this.orchestrator.process({ + entity: unprocessedEntity, + state, + }); - track.markProcessorsCompleted(result); + track.markProcessorsCompleted(result); - if (result.ok) { - const { ttl: _, ...stateWithoutTtl } = state ?? {}; - if ( - stableStringify(stateWithoutTtl) !== stableStringify(result.state) - ) { + if (result.ok) { + const { ttl: _, ...stateWithoutTtl } = state ?? {}; + if ( + stableStringify(stateWithoutTtl) !== stableStringify(result.state) + ) { + await this.processingDatabase.transaction(async tx => { + await this.processingDatabase.updateEntityCache(tx, { + id, + state: { + ttl: CACHE_TTL, + ...result.state, + }, + }); + }); + } + } else { + const maybeTtl = state?.ttl; + const ttl = Number.isInteger(maybeTtl) ? (maybeTtl as number) : 0; await this.processingDatabase.transaction(async tx => { await this.processingDatabase.updateEntityCache(tx, { id, - state: { - ttl: CACHE_TTL, - ...result.state, - }, + state: ttl > 0 ? { ...state, ttl: ttl - 1 } : {}, }); }); } - } else { - const maybeTtl = state?.ttl; - const ttl = Number.isInteger(maybeTtl) ? (maybeTtl as number) : 0; - await this.processingDatabase.transaction(async tx => { - await this.processingDatabase.updateEntityCache(tx, { - id, - state: ttl > 0 ? { ...state, ttl: ttl - 1 } : {}, + + const location = + unprocessedEntity?.metadata?.annotations?.[ANNOTATION_LOCATION]; + for (const error of result.errors) { + this.logger.warn(error.message, { + entity: entityRef, + location, }); - }); - } + } + const errorsString = JSON.stringify( + result.errors.map(e => serializeError(e)), + ); - const location = - unprocessedEntity?.metadata?.annotations?.[ANNOTATION_LOCATION]; - for (const error of result.errors) { - this.logger.warn(error.message, { - entity: entityRef, - location, - }); - } - const errorsString = JSON.stringify( - result.errors.map(e => serializeError(e)), - ); + let hashBuilder = this.createHash().update(errorsString); - let hashBuilder = this.createHash().update(errorsString); - - if (result.ok) { - const { entityRefs: parents } = - await this.processingDatabase.transaction(tx => - this.processingDatabase.listParents(tx, { - entityRef, - }), - ); - - hashBuilder = hashBuilder - .update(stableStringify({ ...result.completedEntity })) - .update(stableStringify([...result.deferredEntities])) - .update(stableStringify([...result.relations])) - .update(stableStringify([...result.refreshKeys])) - .update(stableStringify([...parents])); - } - - const resultHash = hashBuilder.digest('hex'); - if (resultHash === previousResultHash) { - // If nothing changed in our produced outputs, we cannot have any - // significant effect on our surroundings; therefore, we just abort - // without any updates / stitching. - track.markSuccessfulWithNoChanges(); - return; - } - - // If the result was marked as not OK, it signals that some part of the - // processing pipeline threw an exception. This can happen both as part of - // non-catastrophic things such as due to validation errors, as well as if - // something fatal happens inside the processing for other reasons. In any - // case, this means we can't trust that anything in the output is okay. So - // just store the errors and trigger a stich so that they become visible to - // the outside. - if (!result.ok) { - // notify the error listener if the entity can not be processed. - Promise.resolve(undefined) - .then(() => - this.onProcessingError?.({ - unprocessedEntity, - errors: result.errors, - }), - ) - .catch(error => { - this.logger.debug( - `Processing error listener threw an exception, ${stringifyError( - error, - )}`, + if (result.ok) { + const { entityRefs: parents } = + await this.processingDatabase.transaction(tx => + this.processingDatabase.listParents(tx, { + entityRef, + }), ); - }); + hashBuilder = hashBuilder + .update(stableStringify({ ...result.completedEntity })) + .update(stableStringify([...result.deferredEntities])) + .update(stableStringify([...result.relations])) + .update(stableStringify([...result.refreshKeys])) + .update(stableStringify([...parents])); + } + + const resultHash = hashBuilder.digest('hex'); + if (resultHash === previousResultHash) { + // If nothing changed in our produced outputs, we cannot have any + // significant effect on our surroundings; therefore, we just abort + // without any updates / stitching. + track.markSuccessfulWithNoChanges(); + span.end(); + return; + } + + // If the result was marked as not OK, it signals that some part of the + // processing pipeline threw an exception. This can happen both as part of + // non-catastrophic things such as due to validation errors, as well as if + // something fatal happens inside the processing for other reasons. In any + // case, this means we can't trust that anything in the output is okay. So + // just store the errors and trigger a stich so that they become visible to + // the outside. + if (!result.ok) { + // notify the error listener if the entity can not be processed. + Promise.resolve(undefined) + .then(() => + this.onProcessingError?.({ + unprocessedEntity, + errors: result.errors, + }), + ) + .catch(error => { + this.logger.debug( + `Processing error listener threw an exception, ${stringifyError( + error, + )}`, + ); + }); + + await this.processingDatabase.transaction(async tx => { + await this.processingDatabase.updateProcessedEntityErrors(tx, { + id, + errors: errorsString, + resultHash, + }); + }); + await this.stitcher.stitch( + new Set([stringifyEntityRef(unprocessedEntity)]), + ); + track.markSuccessfulWithErrors(); + span.setStatus({ code: SpanStatusCode.ERROR }); + span.end(); + return; + } + + result.completedEntity.metadata.uid = id; + let oldRelationSources: Map; await this.processingDatabase.transaction(async tx => { - await this.processingDatabase.updateProcessedEntityErrors(tx, { - id, - errors: errorsString, - resultHash, - }); + const { previous } = + await this.processingDatabase.updateProcessedEntity(tx, { + id, + processedEntity: result.completedEntity, + resultHash, + errors: errorsString, + relations: result.relations, + deferredEntities: result.deferredEntities, + locationKey, + refreshKeys: result.refreshKeys, + }); + oldRelationSources = new Map( + previous.relations.map(r => [ + `${r.source_entity_ref}:${r.type}`, + r.source_entity_ref, + ]), + ); }); - await this.stitcher.stitch( - new Set([stringifyEntityRef(unprocessedEntity)]), + + const newRelationSources = new Map( + result.relations.map(relation => { + const sourceEntityRef = stringifyEntityRef(relation.source); + return [`${sourceEntityRef}:${relation.type}`, sourceEntityRef]; + }), ); - track.markSuccessfulWithErrors(); - return; + + const setOfThingsToStitch = new Set([ + stringifyEntityRef(result.completedEntity), + ]); + newRelationSources.forEach((sourceEntityRef, uniqueKey) => { + if (!oldRelationSources.has(uniqueKey)) { + setOfThingsToStitch.add(sourceEntityRef); + } + }); + oldRelationSources!.forEach((sourceEntityRef, uniqueKey) => { + if (!newRelationSources.has(uniqueKey)) { + setOfThingsToStitch.add(sourceEntityRef); + } + }); + + await this.stitcher.stitch(setOfThingsToStitch); + + track.markSuccessfulWithChanges(setOfThingsToStitch.size); + } catch (error) { + assertError(error); + track.markFailed(error); + span.recordException(error); + span.setStatus({ code: SpanStatusCode.ERROR }); } - - result.completedEntity.metadata.uid = id; - let oldRelationSources: Map; - await this.processingDatabase.transaction(async tx => { - const { previous } = - await this.processingDatabase.updateProcessedEntity(tx, { - id, - processedEntity: result.completedEntity, - resultHash, - errors: errorsString, - relations: result.relations, - deferredEntities: result.deferredEntities, - locationKey, - refreshKeys: result.refreshKeys, - }); - oldRelationSources = new Map( - previous.relations.map(r => [ - `${r.source_entity_ref}:${r.type}`, - r.source_entity_ref, - ]), - ); - }); - - const newRelationSources = new Map( - result.relations.map(relation => { - const sourceEntityRef = stringifyEntityRef(relation.source); - return [`${sourceEntityRef}:${relation.type}`, sourceEntityRef]; - }), - ); - - const setOfThingsToStitch = new Set([ - stringifyEntityRef(result.completedEntity), - ]); - newRelationSources.forEach((sourceEntityRef, uniqueKey) => { - if (!oldRelationSources.has(uniqueKey)) { - setOfThingsToStitch.add(sourceEntityRef); - } - }); - oldRelationSources!.forEach((sourceEntityRef, uniqueKey) => { - if (!newRelationSources.has(uniqueKey)) { - setOfThingsToStitch.add(sourceEntityRef); - } - }); - - await this.stitcher.stitch(setOfThingsToStitch); - - track.markSuccessfulWithChanges(setOfThingsToStitch.size); - } catch (error) { - assertError(error); - track.markFailed(error); - } + span.end(); + }); }, }); } diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index d177a7d4a8..2b4102677e 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Span, SpanStatusCode, trace } from '@opentelemetry/api'; import { Entity, EntityPolicy, @@ -55,6 +56,9 @@ import { } from './util'; import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; import { ProcessorCacheManager } from './ProcessorCacheManager'; +import { addEntityAttributes, TRACER_ID } from '../util/opentelemetry'; + +const tracer = trace.getTracer(TRACER_ID); type Context = { entityRef: string; @@ -64,6 +68,18 @@ type Context = { cache: ProcessorCacheManager; }; +function addProcessorAttributes( + span: Span, + stage: string, + processor: CatalogProcessor, +) { + span.setAttribute('backstage.catalog.processor.stage', stage); + span.setAttribute( + 'backstage.catalog.processor.name', + processor.getProcessorName(), + ); +} + /** @public */ export class DefaultCatalogProcessingOrchestrator implements CatalogProcessingOrchestrator @@ -183,20 +199,30 @@ export class DefaultCatalogProcessingOrchestrator for (const processor of this.options.processors) { if (processor.preProcessEntity) { - try { - res = await processor.preProcessEntity( - res, - context.location, - context.collector.forProcessor(processor), - context.originLocation, - context.cache.forProcessor(processor), - ); - } catch (e) { - throw new InputError( - `Processor ${processor.constructor.name} threw an error while preprocessing`, - e, - ); - } + let innerRes = res; + res = await tracer.startActiveSpan('ProcessingStep', async span => { + addEntityAttributes(span, context.entityRef); + addProcessorAttributes(span, 'preProcessEntity', processor); + try { + innerRes = await processor.preProcessEntity!( + innerRes, + context.location, + context.collector.forProcessor(processor), + context.originLocation, + context.cache.forProcessor(processor), + ); + } catch (e) { + span.recordException(e); + span.setStatus({ code: SpanStatusCode.ERROR }); + span.end(); + throw new InputError( + `Processor ${processor.constructor.name} threw an error while preprocessing`, + e, + ); + } + span.end(); + return innerRes; + }); } } @@ -361,19 +387,29 @@ export class DefaultCatalogProcessingOrchestrator for (const processor of this.options.processors) { if (processor.postProcessEntity) { - try { - res = await processor.postProcessEntity( - res, - context.location, - context.collector.forProcessor(processor), - context.cache.forProcessor(processor), - ); - } catch (e) { - throw new InputError( - `Processor ${processor.constructor.name} threw an error while postprocessing`, - e, - ); - } + let innerRes = res; + res = await tracer.startActiveSpan('ProcessingStep', async span => { + addEntityAttributes(span, context.entityRef); + addProcessorAttributes(span, 'postProcessEntity', processor); + try { + innerRes = await processor.postProcessEntity!( + innerRes, + context.location, + context.collector.forProcessor(processor), + context.cache.forProcessor(processor), + ); + } catch (e) { + span.recordException(e); + span.setStatus({ code: SpanStatusCode.ERROR }); + span.end(); + throw new InputError( + `Processor ${processor.constructor.name} threw an error while postprocessing`, + e, + ); + } + span.end(); + return innerRes; + }); } } diff --git a/plugins/catalog-backend/src/util/opentelemetry.ts b/plugins/catalog-backend/src/util/opentelemetry.ts new file mode 100644 index 0000000000..16ec276a79 --- /dev/null +++ b/plugins/catalog-backend/src/util/opentelemetry.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Span, SpanStatusCode } from '@opentelemetry/api'; +import { parseEntityRef } from '@backstage/catalog-model'; + +export const TRACER_ID = 'backstage-plugin-catalog-backend'; + +export function addEntityAttributes(span: Span, entityRef: string) { + try { + const fields = parseEntityRef(entityRef); + span.setAttribute('backstage.entity.kind', fields.kind); + span.setAttribute('backstage.entity.namespace', fields.namespace); + span.setAttribute('backstage.entity.name', fields.name); + } catch (err) { + span.recordException(err); + span.setStatus({ code: SpanStatusCode.ERROR }); + } +} From c1d8f44180bf5a30eaff492d8b1f8ecf97d31241 Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Thu, 18 May 2023 21:01:35 +0100 Subject: [PATCH 002/329] refactor: Pull out withActiveSpan to ensure we always end the span regardless of exception Signed-off-by: Mike Bryant --- .../DefaultCatalogProcessingEngine.ts | 19 ++++---- .../DefaultCatalogProcessingOrchestrator.ts | 18 +++---- .../catalog-backend/src/util/opentelemetry.ts | 47 ++++++++++++++++++- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index e9f87c36c7..2ae104666f 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -23,7 +23,7 @@ import { assertError, serializeError, stringifyError } from '@backstage/errors'; import { Hash } from 'crypto'; import stableStringify from 'fast-json-stable-stringify'; import { Logger } from 'winston'; -import { metrics, SpanStatusCode, trace } from '@opentelemetry/api'; +import { metrics, trace } from '@opentelemetry/api'; import { ProcessingDatabase, RefreshStateItem } from '../database/types'; import { createCounterMetric, createSummaryMetric } from '../util/metrics'; import { @@ -35,7 +35,11 @@ import { Stitcher } from '../stitching/Stitcher'; import { startTaskPipeline } from './TaskPipeline'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; -import { addEntityAttributes, TRACER_ID } from '../util/opentelemetry'; +import { + addEntityAttributes, + TRACER_ID, + withActiveSpan, +} from '../util/opentelemetry'; const CACHE_TTL = 5; @@ -134,7 +138,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { } }, processTask: async item => { - await tracer.startActiveSpan('ProcessingRun', async span => { + await withActiveSpan(tracer, 'ProcessingRun', async span => { const track = this.tracker.processStart(item, this.logger); addEntityAttributes(span, item.entityRef); @@ -157,7 +161,8 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { if (result.ok) { const { ttl: _, ...stateWithoutTtl } = state ?? {}; if ( - stableStringify(stateWithoutTtl) !== stableStringify(result.state) + stableStringify(stateWithoutTtl) !== + stableStringify(result.state) ) { await this.processingDatabase.transaction(async tx => { await this.processingDatabase.updateEntityCache(tx, { @@ -216,7 +221,6 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { // significant effect on our surroundings; therefore, we just abort // without any updates / stitching. track.markSuccessfulWithNoChanges(); - span.end(); return; } @@ -255,8 +259,6 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { new Set([stringifyEntityRef(unprocessedEntity)]), ); track.markSuccessfulWithErrors(); - span.setStatus({ code: SpanStatusCode.ERROR }); - span.end(); return; } @@ -309,10 +311,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { } catch (error) { assertError(error); track.markFailed(error); - span.recordException(error); - span.setStatus({ code: SpanStatusCode.ERROR }); } - span.end(); }); }, }); diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 2b4102677e..8091c6f120 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -56,7 +56,11 @@ import { } from './util'; import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; import { ProcessorCacheManager } from './ProcessorCacheManager'; -import { addEntityAttributes, TRACER_ID } from '../util/opentelemetry'; +import { + addEntityAttributes, + TRACER_ID, + withActiveSpan, +} from '../util/opentelemetry'; const tracer = trace.getTracer(TRACER_ID); @@ -200,7 +204,7 @@ export class DefaultCatalogProcessingOrchestrator for (const processor of this.options.processors) { if (processor.preProcessEntity) { let innerRes = res; - res = await tracer.startActiveSpan('ProcessingStep', async span => { + res = await withActiveSpan(tracer, 'ProcessingStep', async span => { addEntityAttributes(span, context.entityRef); addProcessorAttributes(span, 'preProcessEntity', processor); try { @@ -212,15 +216,11 @@ export class DefaultCatalogProcessingOrchestrator context.cache.forProcessor(processor), ); } catch (e) { - span.recordException(e); - span.setStatus({ code: SpanStatusCode.ERROR }); - span.end(); throw new InputError( `Processor ${processor.constructor.name} threw an error while preprocessing`, e, ); } - span.end(); return innerRes; }); } @@ -388,7 +388,7 @@ export class DefaultCatalogProcessingOrchestrator for (const processor of this.options.processors) { if (processor.postProcessEntity) { let innerRes = res; - res = await tracer.startActiveSpan('ProcessingStep', async span => { + res = await withActiveSpan(tracer, 'ProcessingStep', async span => { addEntityAttributes(span, context.entityRef); addProcessorAttributes(span, 'postProcessEntity', processor); try { @@ -399,15 +399,11 @@ export class DefaultCatalogProcessingOrchestrator context.cache.forProcessor(processor), ); } catch (e) { - span.recordException(e); - span.setStatus({ code: SpanStatusCode.ERROR }); - span.end(); throw new InputError( `Processor ${processor.constructor.name} threw an error while postprocessing`, e, ); } - span.end(); return innerRes; }); } diff --git a/plugins/catalog-backend/src/util/opentelemetry.ts b/plugins/catalog-backend/src/util/opentelemetry.ts index 16ec276a79..391869deb8 100644 --- a/plugins/catalog-backend/src/util/opentelemetry.ts +++ b/plugins/catalog-backend/src/util/opentelemetry.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Span, SpanStatusCode } from '@opentelemetry/api'; +import { Span, SpanOptions, SpanStatusCode, Tracer } from '@opentelemetry/api'; import { parseEntityRef } from '@backstage/catalog-model'; export const TRACER_ID = 'backstage-plugin-catalog-backend'; @@ -30,3 +30,48 @@ export function addEntityAttributes(span: Span, entityRef: string) { span.setStatus({ code: SpanStatusCode.ERROR }); } } + +// Adapted from https://github.com/open-telemetry/opentelemetry-js/blob/359fbcc40a859057a02b14e84599eac399b8dba7/api/src/trace/SugaredTracer.ts +// While waiting for something like https://github.com/open-telemetry/opentelemetry-js/pull/3317 to land upstream + +const onException = (e: Error, span: Span) => { + span.recordException(e); + span.setStatus({ + code: SpanStatusCode.ERROR, + }); +}; + +function handleFn ReturnType>( + span: Span, + fn: F, +): ReturnType { + try { + const ret = fn(span) as Promise>; + // if fn is an async function attach a recordException and spanEnd callback to the promise + if (typeof ret.then === 'function' && typeof ret.catch === 'function') { + return ret + .catch((e: Error) => { + onException(e, span); + throw e; + }) + .finally(() => span.end()) as ReturnType; + } + span.end(); + return ret as ReturnType; + } catch (e) { + onException(e, span); + span.end(); + throw e; + } +} + +export function withActiveSpan ReturnType>( + tracer: Tracer, + name: string, + fn: F, + spanOptions: SpanOptions = {}, +): ReturnType { + return tracer.startActiveSpan(name, spanOptions, (span: Span) => { + return handleFn(span, fn); + }); +} From db2cac67450cd75c95ce1ed761b2c6f0993f7bde Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Thu, 18 May 2023 21:06:51 +0100 Subject: [PATCH 003/329] feat: Add ProcessingStage span Signed-off-by: Mike Bryant --- .../DefaultCatalogProcessingOrchestrator.ts | 112 ++++++++++-------- 1 file changed, 63 insertions(+), 49 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 8091c6f120..8b4f4127f3 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -199,34 +199,41 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - let res = entity; + return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { + addEntityAttributes(stageSpan, context.entityRef); + stageSpan.setAttribute( + 'backstage.catalog.processor.stage', + 'preProcessEntity', + ); + let res = entity; - for (const processor of this.options.processors) { - if (processor.preProcessEntity) { - let innerRes = res; - res = await withActiveSpan(tracer, 'ProcessingStep', async span => { - addEntityAttributes(span, context.entityRef); - addProcessorAttributes(span, 'preProcessEntity', processor); - try { - innerRes = await processor.preProcessEntity!( - innerRes, - context.location, - context.collector.forProcessor(processor), - context.originLocation, - context.cache.forProcessor(processor), - ); - } catch (e) { - throw new InputError( - `Processor ${processor.constructor.name} threw an error while preprocessing`, - e, - ); - } - return innerRes; - }); + for (const processor of this.options.processors) { + if (processor.preProcessEntity) { + let innerRes = res; + res = await withActiveSpan(tracer, 'ProcessingStep', async span => { + addEntityAttributes(span, context.entityRef); + addProcessorAttributes(span, 'preProcessEntity', processor); + try { + innerRes = await processor.preProcessEntity!( + innerRes, + context.location, + context.collector.forProcessor(processor), + context.originLocation, + context.cache.forProcessor(processor), + ); + } catch (e) { + throw new InputError( + `Processor ${processor.constructor.name} threw an error while preprocessing`, + e, + ); + } + return innerRes; + }); + } } - } - return res; + return res; + }); } /** @@ -383,32 +390,39 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - let res = entity; + return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { + addEntityAttributes(stageSpan, context.entityRef); + stageSpan.setAttribute( + 'backstage.catalog.processor.stage', + 'postProcessEntity', + ); + let res = entity; - for (const processor of this.options.processors) { - if (processor.postProcessEntity) { - let innerRes = res; - res = await withActiveSpan(tracer, 'ProcessingStep', async span => { - addEntityAttributes(span, context.entityRef); - addProcessorAttributes(span, 'postProcessEntity', processor); - try { - innerRes = await processor.postProcessEntity!( - innerRes, - context.location, - context.collector.forProcessor(processor), - context.cache.forProcessor(processor), - ); - } catch (e) { - throw new InputError( - `Processor ${processor.constructor.name} threw an error while postprocessing`, - e, - ); - } - return innerRes; - }); + for (const processor of this.options.processors) { + if (processor.postProcessEntity) { + let innerRes = res; + res = await withActiveSpan(tracer, 'ProcessingStep', async span => { + addEntityAttributes(span, context.entityRef); + addProcessorAttributes(span, 'postProcessEntity', processor); + try { + innerRes = await processor.postProcessEntity!( + innerRes, + context.location, + context.collector.forProcessor(processor), + context.cache.forProcessor(processor), + ); + } catch (e) { + throw new InputError( + `Processor ${processor.constructor.name} threw an error while postprocessing`, + e, + ); + } + return innerRes; + }); + } } - } - return res; + return res; + }); } } From dae8956754cad162bf20f49c50a62b65be87e4ac Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Thu, 18 May 2023 21:15:41 +0100 Subject: [PATCH 004/329] feat: Add tracing to validate stage Signed-off-by: Mike Bryant --- .../DefaultCatalogProcessingOrchestrator.ts | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 8b4f4127f3..aabe703806 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -269,50 +269,65 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - // Double check that none of the previous steps tried to change something - // related to the entity ref, which would break downstream - if (stringifyEntityRef(entity) !== context.entityRef) { - throw new ConflictError( - 'Fatal: The entity kind, namespace, or name changed during processing', + return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { + addEntityAttributes(stageSpan, context.entityRef); + stageSpan.setAttribute( + 'backstage.catalog.processor.stage', + 'validateEntity', ); - } + // Double check that none of the previous steps tried to change something + // related to the entity ref, which would break downstream + if (stringifyEntityRef(entity) !== context.entityRef) { + throw new ConflictError( + 'Fatal: The entity kind, namespace, or name changed during processing', + ); + } - // Validate that the end result is a valid Entity at all - try { - validateEntity(entity); - } catch (e) { - throw new ConflictError( - `Entity envelope for ${context.entityRef} failed validation after preprocessing`, - e, - ); - } + // Validate that the end result is a valid Entity at all + try { + validateEntity(entity); + } catch (e) { + throw new ConflictError( + `Entity envelope for ${context.entityRef} failed validation after preprocessing`, + e, + ); + } - let valid = false; + let valid = false; - for (const processor of this.options.processors) { - if (processor.validateEntityKind) { - try { - const thisValid = await processor.validateEntityKind(entity); - if (thisValid) { - valid = true; - if (this.options.legacySingleProcessorValidation) { - break; + for (const processor of this.options.processors) { + if (processor.validateEntityKind) { + try { + const thisValid = await withActiveSpan( + tracer, + 'ProcessingStep', + async span => { + addEntityAttributes(span, context.entityRef); + addProcessorAttributes(span, 'postProcessEntity', processor); + return await processor.validateEntityKind(entity); + }, + ); + if (thisValid) { + valid = true; + if (this.options.legacySingleProcessorValidation) { + break; + } } + } catch (e) { + throw new InputError( + `Processor ${processor.constructor.name} threw an error while validating the entity ${context.entityRef}`, + e, + ); } - } catch (e) { - throw new InputError( - `Processor ${processor.constructor.name} threw an error while validating the entity ${context.entityRef}`, - e, - ); } } - } - if (!valid) { - throw new InputError( - `No processor recognized the entity ${context.entityRef} as valid, possibly caused by a foreign kind or apiVersion`, - ); - } + if (!valid) { + throw new InputError( + `No processor recognized the entity ${context.entityRef} as valid, possibly caused by a foreign kind or apiVersion`, + ); + } + }); } /** From 9550ae9ad4378b850f3c4c922244eace5b1c3d2c Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Thu, 18 May 2023 21:19:11 +0100 Subject: [PATCH 005/329] feat: Add span for policy stage Signed-off-by: Mike Bryant --- .../DefaultCatalogProcessingOrchestrator.ts | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index aabe703806..6fa44db1a6 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -240,26 +240,33 @@ export class DefaultCatalogProcessingOrchestrator * Enforce entity policies making sure that entities conform to a general schema */ private async runPolicyStep(entity: Entity): Promise { - let policyEnforcedEntity: Entity | undefined; - - try { - policyEnforcedEntity = await this.options.policy.enforce(entity); - } catch (e) { - throw new InputError( - `Policy check failed for ${stringifyEntityRef(entity)}`, - e, + return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { + addEntityAttributes(stageSpan, stringifyEntityRef(entity)); + stageSpan.setAttribute( + 'backstage.catalog.processor.stage', + 'enforcePolicyEntity', ); - } + let policyEnforcedEntity: Entity | undefined; - if (!policyEnforcedEntity) { - throw new Error( - `Policy unexpectedly returned no data for ${stringifyEntityRef( - entity, - )}`, - ); - } + try { + policyEnforcedEntity = await this.options.policy.enforce(entity); + } catch (e) { + throw new InputError( + `Policy check failed for ${stringifyEntityRef(entity)}`, + e, + ); + } - return policyEnforcedEntity; + if (!policyEnforcedEntity) { + throw new Error( + `Policy unexpectedly returned no data for ${stringifyEntityRef( + entity, + )}`, + ); + } + + return policyEnforcedEntity; + }); } /** From bb5ab706d7e6d8413bf74d7243e5de70a5331e47 Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Thu, 18 May 2023 21:24:50 +0100 Subject: [PATCH 006/329] feat: Add spans for read steps Signed-off-by: Mike Bryant --- .../DefaultCatalogProcessingOrchestrator.ts | 122 ++++++++++-------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 6fa44db1a6..91a1c2f9a6 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -344,65 +344,81 @@ export class DefaultCatalogProcessingOrchestrator entity: LocationEntity, context: Context, ): Promise { - const { type = context.location.type, presence = 'required' } = entity.spec; - const targets = new Array(); - if (entity.spec.target) { - targets.push(entity.spec.target); - } - if (entity.spec.targets) { - targets.push(...entity.spec.targets); - } - - for (const maybeRelativeTarget of targets) { - if (type === 'file' && maybeRelativeTarget.endsWith(path.sep)) { - context.collector.generic()( - processingResult.inputError( - context.location, - `LocationEntityProcessor cannot handle ${type} type location with target ${context.location.target} that ends with a path separator`, - ), - ); - continue; - } - const target = toAbsoluteUrl( - this.options.integrations, - context.location, - type, - maybeRelativeTarget, + return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { + addEntityAttributes(stageSpan, context.entityRef); + stageSpan.setAttribute( + 'backstage.catalog.processor.stage', + 'readLocationEntity', ); + const { type = context.location.type, presence = 'required' } = + entity.spec; + const targets = new Array(); + if (entity.spec.target) { + targets.push(entity.spec.target); + } + if (entity.spec.targets) { + targets.push(...entity.spec.targets); + } - let didRead = false; - for (const processor of this.options.processors) { - if (processor.readLocation) { - try { - const read = await processor.readLocation( - { - type, - target, - presence, - }, - presence === 'optional', - context.collector.forProcessor(processor), - this.options.parser, - context.cache.forProcessor(processor, target), - ); - if (read) { - didRead = true; - break; + for (const maybeRelativeTarget of targets) { + if (type === 'file' && maybeRelativeTarget.endsWith(path.sep)) { + context.collector.generic()( + processingResult.inputError( + context.location, + `LocationEntityProcessor cannot handle ${type} type location with target ${context.location.target} that ends with a path separator`, + ), + ); + continue; + } + const target = toAbsoluteUrl( + this.options.integrations, + context.location, + type, + maybeRelativeTarget, + ); + + let didRead = false; + for (const processor of this.options.processors) { + if (processor.readLocation) { + try { + const read = await withActiveSpan( + tracer, + 'ProcessingStep', + async span => { + addEntityAttributes(span, context.entityRef); + addProcessorAttributes(span, 'readLocationEntity', processor); + return await processor.readLocation( + { + type, + target, + presence, + }, + presence === 'optional', + context.collector.forProcessor(processor), + this.options.parser, + context.cache.forProcessor(processor, target), + ); + }, + ); + if (read) { + didRead = true; + break; + } + } catch (e) { + throw new InputError( + `Processor ${processor.constructor.name} threw an error while reading ${type}:${target}`, + e, + ); } - } catch (e) { - throw new InputError( - `Processor ${processor.constructor.name} threw an error while reading ${type}:${target}`, - e, - ); } } + if (!didRead) { + throw new InputError( + `No processor was able to handle reading of ${type}:${target}`, + ); + } } - if (!didRead) { - throw new InputError( - `No processor was able to handle reading of ${type}:${target}`, - ); - } - } + }); } /** From 5060c6ed7d04b9875a4c50b37a5b33d467e45a69 Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Thu, 18 May 2023 22:52:00 +0100 Subject: [PATCH 007/329] fix: Fix tsc errors Signed-off-by: Mike Bryant --- .../src/processing/DefaultCatalogProcessingOrchestrator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 91a1c2f9a6..dfca5d2058 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Span, SpanStatusCode, trace } from '@opentelemetry/api'; +import { Span, trace } from '@opentelemetry/api'; import { Entity, EntityPolicy, @@ -311,7 +311,7 @@ export class DefaultCatalogProcessingOrchestrator async span => { addEntityAttributes(span, context.entityRef); addProcessorAttributes(span, 'postProcessEntity', processor); - return await processor.validateEntityKind(entity); + return await processor.validateEntityKind!(entity); }, ); if (thisValid) { @@ -387,7 +387,7 @@ export class DefaultCatalogProcessingOrchestrator async span => { addEntityAttributes(span, context.entityRef); addProcessorAttributes(span, 'readLocationEntity', processor); - return await processor.readLocation( + return await processor.readLocation!( { type, target, From ee48005f1871de089ec9eedb85faf0caf3290b5f Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Thu, 18 May 2023 23:15:24 +0100 Subject: [PATCH 008/329] fix: Supply objects that satisfy the type constraints in tests Signed-off-by: Mike Bryant --- .../processing/DefaultCatalogProcessingOrchestrator.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts index ceb6992d7a..fc6855f6df 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -194,10 +194,12 @@ describe('DefaultCatalogProcessingOrchestrator', () => { it('runs all processor validations when asked to', async () => { const validate = jest.fn(async () => true); - const processor1: Partial = { + const processor1: CatalogProcessor = { + getProcessorName: () => 'processor1', validateEntityKind: validate, }; - const processor2: Partial = { + const processor2: CatalogProcessor = { + getProcessorName: () => 'processor2', validateEntityKind: validate, }; From b5de6d107f026d9ab7bae72d7c6ce665f92570fc Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 2 Jun 2023 10:15:35 -0400 Subject: [PATCH 009/329] chore: Replace deprecated imports Signed-off-by: Adam Harvey --- docs/features/search/how-to-guides.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 7de37cb624..382221d781 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -51,7 +51,7 @@ The TechDocs plugin has supported integrations to Search, meaning that it provides a default collator factory ready to be used. The purpose of this guide is to walk you through how to register the -[DefaultTechDocsCollatorFactory](https://github.com/backstage/backstage/blob/de294ce5c410c9eb56da6870a1fab795268f60e3/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts) +[DefaultTechDocsCollatorFactory](https://github.com/backstage/backstage/blob/1adc2c7/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts) in your App, so that you can get TechDocs documents indexed. If you have been through the @@ -61,10 +61,10 @@ so, you can go ahead and follow this guide - if not, start by going through the getting started guide. 1. Import the `DefaultTechDocsCollatorFactory` from - `@backstage/plugin-techdocs-backend`. + `@backstage/plugin-search-backend-module-techdocs`. ```typescript - import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; + import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-search-backend-module-techdocs'; ``` 2. If there isn't an existing schedule you'd like to run the collator on, be From f40c8ac5347c0bd145eb1f7c0dc978d9c17f0148 Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Tue, 13 Jun 2023 23:33:50 +0100 Subject: [PATCH 010/329] style: Apply review comments Signed-off-by: Mike Bryant --- .../DefaultCatalogProcessingOrchestrator.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index dfca5d2058..98f9cd32a5 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -201,10 +201,7 @@ export class DefaultCatalogProcessingOrchestrator ): Promise { return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { addEntityAttributes(stageSpan, context.entityRef); - stageSpan.setAttribute( - 'backstage.catalog.processor.stage', - 'preProcessEntity', - ); + stageSpan.setAttribute('backstage.catalog.processor.stage', 'preProcess'); let res = entity; for (const processor of this.options.processors) { @@ -244,7 +241,7 @@ export class DefaultCatalogProcessingOrchestrator addEntityAttributes(stageSpan, stringifyEntityRef(entity)); stageSpan.setAttribute( 'backstage.catalog.processor.stage', - 'enforcePolicyEntity', + 'enforcePolicy', ); let policyEnforcedEntity: Entity | undefined; @@ -278,10 +275,7 @@ export class DefaultCatalogProcessingOrchestrator ): Promise { return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { addEntityAttributes(stageSpan, context.entityRef); - stageSpan.setAttribute( - 'backstage.catalog.processor.stage', - 'validateEntity', - ); + stageSpan.setAttribute('backstage.catalog.processor.stage', 'validate'); // Double check that none of the previous steps tried to change something // related to the entity ref, which would break downstream if (stringifyEntityRef(entity) !== context.entityRef) { @@ -310,7 +304,7 @@ export class DefaultCatalogProcessingOrchestrator 'ProcessingStep', async span => { addEntityAttributes(span, context.entityRef); - addProcessorAttributes(span, 'postProcessEntity', processor); + addProcessorAttributes(span, 'validateEntityKind', processor); return await processor.validateEntityKind!(entity); }, ); @@ -348,7 +342,7 @@ export class DefaultCatalogProcessingOrchestrator addEntityAttributes(stageSpan, context.entityRef); stageSpan.setAttribute( 'backstage.catalog.processor.stage', - 'readLocationEntity', + 'readLocation', ); const { type = context.location.type, presence = 'required' } = entity.spec; @@ -386,7 +380,7 @@ export class DefaultCatalogProcessingOrchestrator 'ProcessingStep', async span => { addEntityAttributes(span, context.entityRef); - addProcessorAttributes(span, 'readLocationEntity', processor); + addProcessorAttributes(span, 'readLocation', processor); return await processor.readLocation!( { type, From ab51b0c9aee7ee298b99f32aedba573f251e49ee Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Wed, 14 Jun 2023 00:09:02 +0100 Subject: [PATCH 011/329] style: Apply review comments Signed-off-by: Mike Bryant --- .changeset/slimy-kids-jam.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/slimy-kids-jam.md b/.changeset/slimy-kids-jam.md index ea2897bed1..c02daf325e 100644 --- a/.changeset/slimy-kids-jam.md +++ b/.changeset/slimy-kids-jam.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend': minor --- Added OpenTelemetry spans for catalog processing From d440f1dd0e7277af76ee4df0f60010b574e1cbb7 Mon Sep 17 00:00:00 2001 From: Brian Forbis Date: Sat, 17 Jun 2023 17:52:25 -0400 Subject: [PATCH 012/329] Add linguist tags processor Signed-off-by: Brian Forbis --- .changeset/large-badgers-switch.md | 5 + .changeset/strange-shrimps-mix.md | 5 + plugins/linguist-backend/README.md | 126 +++- plugins/linguist-backend/api-report.md | 41 + plugins/linguist-backend/config.d.ts | 46 ++ plugins/linguist-backend/package.json | 6 +- plugins/linguist-backend/src/index.ts | 1 + .../processor/LinguistTagsProcessor.test.ts | 343 +++++++++ .../src/processor/LinguistTagsProcessor.ts | 269 +++++++ .../LinguistTagsProcessor.test.ts.snap | 707 ++++++++++++++++++ .../linguist-backend/src/processor/index.ts | 20 + plugins/linguist-common/api-report.md | 5 +- plugins/linguist-common/src/types.ts | 5 +- yarn.lock | 2 + 14 files changed, 1577 insertions(+), 4 deletions(-) create mode 100644 .changeset/large-badgers-switch.md create mode 100644 .changeset/strange-shrimps-mix.md create mode 100644 plugins/linguist-backend/config.d.ts create mode 100644 plugins/linguist-backend/src/processor/LinguistTagsProcessor.test.ts create mode 100644 plugins/linguist-backend/src/processor/LinguistTagsProcessor.ts create mode 100644 plugins/linguist-backend/src/processor/__snapshots__/LinguistTagsProcessor.test.ts.snap create mode 100644 plugins/linguist-backend/src/processor/index.ts diff --git a/.changeset/large-badgers-switch.md b/.changeset/large-badgers-switch.md new file mode 100644 index 0000000000..47c06a4afe --- /dev/null +++ b/.changeset/large-badgers-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist-common': patch +--- + +Exported new LanguageType type alias diff --git a/.changeset/strange-shrimps-mix.md b/.changeset/strange-shrimps-mix.md new file mode 100644 index 0000000000..fbb08eeeb6 --- /dev/null +++ b/.changeset/strange-shrimps-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist-backend': minor +--- + +Adds a processor to the linguist backend which can automatically add language tags to entities diff --git a/plugins/linguist-backend/README.md b/plugins/linguist-backend/README.md index 6e2a20cf2d..7bca98191b 100644 --- a/plugins/linguist-backend/README.md +++ b/plugins/linguist-backend/README.md @@ -1,6 +1,6 @@ # Linguist Backend -Welcome to the Linguist backend plugin! This plugin provides data for the Linguist frontend features. +Welcome to the Linguist backend plugin! This plugin provides data for the Linguist frontend features. Additionally, it provides an optional entity processor which will automate adding language tags to your entities. ## Setup @@ -144,6 +144,130 @@ return createRouter( **Note:** This has the potential to cause a lot of processing, be very thoughtful about this before hand +## Linguist Tags Processor + +The `LinguistTagsProcessor` can be added into your catalog builder as a way to incorporate the language breakdown from linguist as `metadata.tags` on your entities. Doing so enables the ability to easily filter for entities in your catalog index based on the language of the source repository. + +### Processor Setup + +Setup the linguist tag processor in `packages/backend/src/plugins/catalog.ts`. + +```ts +import { LinguistTagsProcessor } from '@backstage/plugin-linguist-backend'; +// ... +export default async function createPlugin( + // ... + builder.addProcessor( + LinguistTagsProcessor.fromConfig(env.config, { + logger: env.logger, + discovery: env.discovery, + }) + ); +``` + +### Processor Options + +The processor accepts configurations either directly as options when constructing using `fromConfig()`, or can also be configured in `app-config.yaml` with the same fields. + +Example linguist processor configuration: + +```yaml +linguist: + tagsProcessor: + bytesThreshold: 1000 + languageTypes: ['programming', 'markup'] + languageMap: + Dockerfile: '' + TSX: 'react' + cacheTTL: + hours: 24 +``` + +#### `languageMap` + +The `languageMap` option allows you to build a custom map of linguist languages to how you want them to show up as tags. The keys should be exact matches to languages in the [linguist dataset](https://github.com/github-linguist/linguist/blob/master/lib/linguist/languages.yml) and the values should be how they render as backstage tags. These values will be used "as is" and will not be further transformed. + +Keep in mind that backstage has [character requirements for tags](https://backstage.io/docs/features/software-catalog/descriptor-format#tags-optional). If your map emits an invalid tag, it will cause an error during processing and your entity will not be processed. + +If you map a key to `''`, it will not be emitted as a tag. This can be useful if you want to ignore some of the linguist languages. + +```yaml +linguist: + tagsProcessor: + languageMap: + # You don't want dockerfile to show up as a tag + Dockerfile: '' + # Be more specific about what the file is + HCL: terraform + # A more casual tag for a formal name + Protocol Buffer: protobuf +``` + +#### `cacheTTL` + +The `cacheTTL` option allows you to determine for how long this processor will cache languages for an `entityRef` before refreshing from the linguist backend. As this processor will run continuously, this cache is supplied to limit the load done on the linguist DB and API. + +By default, this processor will cache languages for 30 minutes before refreshing from the linguist database. + +You can optionally disable the cache entirely by passing in a `cacheTTL` duration of 0 minutes. + +```yaml +linguist: + tagsProcessor: + cacheTTL: { minutes: 0 } +``` + +#### `bytesThreshold` + +The `bytesThreshold` option allows you to control a number of bytes threshold which must be surpassed before a language tag will be emitted by this processor. As an example, some repositories may have short build scripts written in Bash, but you may only want the main language of the project emitted (an alternate way to control this is to use the `languageMap` to map `Shell` languages to `undefined`). + +```yaml +linguist: + tagsProcessor: + # Ignore languages with less than 5000 bytes in a repo. + bytesThreshold: 5000 +``` + +#### `languageTypes` + +The `languageTypes` option allows you to control what categories of linguist languages are automatically added as tags. By default, this will only include language tags of type `programming`, but you can pass in a custom array here to allow adding other language types. + +You can see the full breakdown of linguist supported languages [in their repo](https://github.com/github-linguist/linguist/blob/master/lib/linguist/languages.yml). + +For example, you may want to also include languages of type `data` + +```yaml +linguist: + tagsProcessor: + languageTypes: + - programming + - data +``` + +#### `shouldProcessEntity` + +The `shouldProcessEntity` is a function you can pass into the processor which determines which entities should have language tags fetched from linguist and added to the entity. By default, this will only run on entities of `kind: Component`, however this function let's you fully customize which entities should be processed. + +As an example, you may choose to extend this to support both `Component` and `Resource` kinds along with allowing an opt-in annotation on the entity which entity authors can use. + +As this option is a function, it cannot be configured in `app-config.yaml`. You must pass this as an option within typescript. + +```ts +LinguistLanguageTagsProcessor.fromConfig(env.config, { + logger: env.logger, + discovery: env.discovery, + shouldProcessEntity: (entity: Entity) => { + if ( + ['Component', 'Resource'].includes(entity.kind) && + entity.metadata.annotations?.['some-custom-annotation'] + ) { + return true; + } + return false; + }, +}); +``` + ## Links - [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/linguist) diff --git a/plugins/linguist-backend/api-report.md b/plugins/linguist-backend/api-report.md index cd2c9cb3f3..aac978e426 100644 --- a/plugins/linguist-backend/api-report.md +++ b/plugins/linguist-backend/api-report.md @@ -4,9 +4,15 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { CatalogProcessor } from '@backstage/plugin-catalog-node'; +import { CatalogProcessorCache } from '@backstage/plugin-catalog-node'; +import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { HumanDuration } from '@backstage/types'; import { Languages } from '@backstage/plugin-linguist-common'; +import { LanguageType } from '@backstage/plugin-linguist-common'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -48,6 +54,38 @@ export interface LinguistPluginOptions { useSourceLocation?: boolean; } +// @public +export class LinguistTagsProcessor implements CatalogProcessor { + constructor(options: LinguistTagsProcessorOptions); + // (undocumented) + static fromConfig( + config: Config, + options: LinguistTagsProcessorOptions, + ): LinguistTagsProcessor; + // (undocumented) + getProcessorName(): string; + preProcessEntity( + entity: Entity, + _: any, + __: any, + ___: any, + cache: CatalogProcessorCache, + ): Promise; +} + +// @public +export interface LinguistTagsProcessorOptions { + bytesThreshold?: number; + cacheTTL?: HumanDuration; + // (undocumented) + discovery: DiscoveryService; + languageMap?: Record; + languageTypes?: LanguageType[]; + // (undocumented) + logger: Logger; + shouldProcessEntity?: ShouldProcessEntity; +} + // @public (undocumented) export interface PluginOptions { // (undocumented) @@ -81,4 +119,7 @@ export interface RouterOptions { // (undocumented) tokenManager: TokenManager; } + +// @public +export type ShouldProcessEntity = (entity: Entity) => boolean; ``` diff --git a/plugins/linguist-backend/config.d.ts b/plugins/linguist-backend/config.d.ts new file mode 100644 index 0000000000..5faf28fb1d --- /dev/null +++ b/plugins/linguist-backend/config.d.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HumanDuration } from '@backstage/types'; + +export interface Config { + /** Configuration options for the linguist plugin */ + linguist?: { + /** Options for the tags processor */ + tagsProcessor?: { + /** + * Determines how many bytes of a language should be in a repo + * for it to be added as an entity tag. Defaults to 0. + */ + bytesThreshold?: number; + /** + * The types of linguist languages that should be processed. Can be + * any of "programming", "data", "markup", "prose". Defaults to ["programming"]. + */ + languageTypes?: string[]; + /** + * A custom mapping of linguist languages to how they should be rendered as entity tags. + * If a language is mapped to '' it will not be included as a tag. + */ + languageMap?: Record; + /** + * How long to cache entity languages for in memory. Used to avoid constant db hits during + * processing. Defaults to 30 minutes. + */ + cacheTTL?: HumanDuration; + }; + }; +} diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json index 0239b241d3..436fe02606 100644 --- a/plugins/linguist-backend/package.json +++ b/plugins/linguist-backend/package.json @@ -30,6 +30,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-linguist-common": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "*", @@ -48,11 +49,14 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", + "js-yaml": "^4.1.0", "msw": "^1.0.0", "supertest": "^6.2.4" }, "files": [ "dist", + "config.d.ts", "migrations/**/*.{js,d.ts}" - ] + ], + "configSchema": "config.d.ts" } diff --git a/plugins/linguist-backend/src/index.ts b/plugins/linguist-backend/src/index.ts index 6ac1c3770a..107877551e 100644 --- a/plugins/linguist-backend/src/index.ts +++ b/plugins/linguist-backend/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export * from './processor'; export * from './service/router'; export type { LinguistBackendApi } from './api'; export { linguistPlugin } from './plugin'; diff --git a/plugins/linguist-backend/src/processor/LinguistTagsProcessor.test.ts b/plugins/linguist-backend/src/processor/LinguistTagsProcessor.test.ts new file mode 100644 index 0000000000..4954820792 --- /dev/null +++ b/plugins/linguist-backend/src/processor/LinguistTagsProcessor.test.ts @@ -0,0 +1,343 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + LinguistTagsProcessor, + LinguistTagsProcessorOptions, + sanitizeTag, +} from './LinguistTagsProcessor'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { CatalogProcessorCache } from '@backstage/plugin-catalog-node'; +import { Entity, makeValidator } from '@backstage/catalog-model'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; +import fetch, { Response } from 'node-fetch'; +import * as path from 'path'; +import yaml from 'js-yaml'; +import * as fs from 'fs'; + +const { isValidTag } = makeValidator(); + +jest.mock('node-fetch', () => jest.fn()); +const mockedFetch: jest.MockedFunction = + fetch as jest.MockedFunction; + +const discovery: DiscoveryService = { + getBaseUrl: jest.fn().mockResolvedValue('http://example.com/api/linguist'), + getExternalBaseUrl: jest.fn(), +}; + +let state: Record = {}; +const mockCacheGet = jest + .fn() + .mockImplementation(async (key: string) => state[key]); +const mockCacheSet = jest.fn().mockImplementation((key: string, value: any) => { + state[key] = value; +}); +const cache: CatalogProcessorCache = { + get: mockCacheGet, + set: mockCacheSet, +}; + +describe('sanitizeTag', () => { + const linguistDataSet = yaml.load( + fs.readFileSync( + path.resolve(require.resolve('linguist-js'), '../../ext/languages.yml'), + 'utf-8', + ), + ) as Object; + const languages = Object.keys(linguistDataSet); + test('Should clean up all linguist languages', () => { + const invalid = languages + .map(sanitizeTag) + .filter(lang => !isValidTag(lang)); + expect(invalid).toStrictEqual([]); + // Keep a snapshot here so that as new languages are added to linguist, + // we can spot check them to make sure the transformer for them makes sense. + expect(languages.map(sanitizeTag)).toMatchSnapshot(); + }); +}); + +describe('LinguistTagsProcessor', () => { + afterEach(() => { + mockedFetch.mockReset(); + mockCacheGet.mockClear(); + mockCacheSet.mockClear(); + state = {}; + }); + + test('Should construct fromConfig', () => { + const config = new ConfigReader({ + linguist: {}, + }); + expect(() => { + return LinguistTagsProcessor.fromConfig(config, { + logger: getVoidLogger(), + discovery, + }); + }).not.toThrow(); + }); + + test('Should assign valid language tags', async () => { + const processor = buildProcessor({}); + + mockFetchImplementation(); + const entity = baseEntity(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(entity.metadata.tags).toStrictEqual([ + 'c++', + 'asp-dot-net', + 'java', + 'common-lisp', + ]); + + entity.metadata.tags?.forEach(tag => { + expect(isValidTag(tag)).toBeTruthy(); + }); + }); + + test('Should not duplicate existing tags', async () => { + const processor = buildProcessor({}); + + mockFetchImplementation(); + const entity = baseEntity(); + entity.metadata.tags = ['existing', 'tags', 'java']; + + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(entity.metadata.tags).toStrictEqual([ + 'existing', + 'tags', + 'java', + 'c++', + 'asp-dot-net', + 'common-lisp', + ]); + }); + + test('Should not process Resource entities by default', async () => { + const processor = buildProcessor({}); + + mockFetchImplementation(); + const entity = baseEntity(); + entity.kind = 'Resource'; + + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(0); + expect(entity.metadata.tags).toStrictEqual(undefined); + }); + + test('Can process Resource entities by overriding shouldProcessEntity', async () => { + const processor = buildProcessor({ + shouldProcessEntity: (entity: Entity) => { + return entity.kind === 'Resource'; + }, + }); + + mockFetchImplementation(); + const entity = baseEntity(); + entity.kind = 'Resource'; + + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(entity.metadata.tags).toStrictEqual([ + 'c++', + 'asp-dot-net', + 'java', + 'common-lisp', + ]); + }); + + test('Can omit languages using languageMap', async () => { + const processor = buildProcessor({ + languageMap: { + Java: '', + 'ASP.net': '', + }, + }); + + mockFetchImplementation(); + const entity = baseEntity(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(entity.metadata.tags).toStrictEqual(['c++', 'common-lisp']); + }); + + test('Can rewrite langs using languageMap', async () => { + const processor = buildProcessor({ + languageMap: { + Java: 'notjava', + }, + }); + + mockFetchImplementation(); + const entity = baseEntity(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(entity.metadata.tags).toStrictEqual([ + 'c++', + 'asp-dot-net', + 'notjava', + 'common-lisp', + ]); + }); + + test('Can omit languages less than bytesThreshold', async () => { + const processor = buildProcessor({ + bytesThreshold: 5000, + }); + + mockFetchImplementation(); + const entity = baseEntity(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(entity.metadata.tags).toStrictEqual(['java', 'common-lisp']); + }); + + test('Can include languages that arent programming', async () => { + const processor = buildProcessor({ + languageTypes: ['data'], + }); + + mockFetchImplementation(); + const entity = baseEntity(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(entity.metadata.tags).toStrictEqual(['yaml', 'json']); + }); + + test('Refetches from API when cache disabled', async () => { + const processor = buildProcessor({ + cacheTTL: { minutes: 0 }, + }); + + mockFetchImplementation(); + const entity = baseEntity(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(mockCacheGet).toHaveBeenCalledTimes(0); + expect(mockCacheSet).toHaveBeenCalledTimes(0); + mockedFetch.mockClear(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(mockCacheGet).toHaveBeenCalledTimes(0); + expect(mockCacheSet).toHaveBeenCalledTimes(0); + }); + + test('Caches across runs with cache enabled', async () => { + const processor = buildProcessor({ + cacheTTL: { minutes: 5 }, + }); + + mockFetchImplementation(); + const entity = baseEntity(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(mockCacheGet).toHaveBeenCalledTimes(1); + expect(mockCacheSet).toHaveBeenCalledTimes(1); + + mockedFetch.mockClear(); + mockCacheGet.mockClear(); + mockCacheSet.mockClear(); + await processor.preProcessEntity(entity, null, null, null, cache); + expect(mockedFetch).toHaveBeenCalledTimes(0); + expect(mockCacheGet).toHaveBeenCalledTimes(1); + expect(mockCacheSet).toHaveBeenCalledTimes(0); + }); +}); + +function mockFetchImplementation(): void { + mockedFetch.mockResolvedValue({ + json: jest.fn().mockResolvedValue({ + languageCount: 6, + totalBytes: 43823, + processedDate: '2023-06-20T21:37:48.337Z', + breakdown: [ + { + name: 'YAML', + percentage: 2.23, + bytes: 979, + type: 'data', + color: '#cb171e', + }, + { + name: 'JSON', + percentage: 1.31, + bytes: 574, + type: 'data', + color: '#292929', + }, + { + name: 'C++', + percentage: 5.25, + bytes: 2300, + type: 'programming', + color: '#f34b7d', + }, + { + name: 'ASP.net', + percentage: 6.97, + bytes: 3053, + type: 'programming', + color: '#178600', + }, + { + name: 'Java', + percentage: 12.79, + bytes: 5603, + type: 'programming', + color: '#b07219', + }, + { + name: 'Common Lisp', + percentage: 71.46, + bytes: 31314, + type: 'programming', + color: '#3fb68b', + }, + ], + }), + } as unknown as Response); +} + +function baseEntity(): Entity { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'foo', + }, + }; +} + +function buildProcessor(options: Partial) { + const config = new ConfigReader({ + linguist: { + tagsProcessor: { + bytesThreshold: options.bytesThreshold, + languageTypes: options.languageTypes, + languageMap: options.languageMap, + cacheTTL: options.cacheTTL, + }, + }, + }); + return LinguistTagsProcessor.fromConfig(config, { + logger: getVoidLogger(), + discovery, + shouldProcessEntity: options.shouldProcessEntity, + }); +} diff --git a/plugins/linguist-backend/src/processor/LinguistTagsProcessor.ts b/plugins/linguist-backend/src/processor/LinguistTagsProcessor.ts new file mode 100644 index 0000000000..b27fe2c5e9 --- /dev/null +++ b/plugins/linguist-backend/src/processor/LinguistTagsProcessor.ts @@ -0,0 +1,269 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { + CatalogProcessor, + CatalogProcessorCache, +} from '@backstage/plugin-catalog-node'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; +import { Languages, LanguageType } from '@backstage/plugin-linguist-common'; +import fetch from 'node-fetch'; +import { Logger } from 'winston'; +import { HumanDuration, durationToMilliseconds } from '@backstage/types'; +import { Config } from '@backstage/config'; + +/** + * A function which given an entity, determines if it should be processed for linguist tags. + * @public + */ +export type ShouldProcessEntity = (entity: Entity) => boolean; + +interface CachedData { + [key: string]: number | string[]; + languages: string[]; + cachedTime: number; +} + +/** + * The constructor options for building the LinguistTagsProcessor + * @public + */ +export interface LinguistTagsProcessorOptions { + logger: Logger; + discovery: DiscoveryService; + /** + * Optional map that gives full control over which linguist languages should be included as tags and + * how they should be represented. The keys should be exact matches to languages in the linguist + * and the values should be how they render as backstage tags. Keep in mind that backstage has character + * requirements for tags. If you map a key to a falsey value, it will not be emitted as a tag. + */ + languageMap?: Record; + /** + * A function which determines which entities should be processed by the LinguistTagProcessor. + * + * The default is to process all entities of kind=Component + */ + shouldProcessEntity?: ShouldProcessEntity; + /** + * Determines how long to cache language breakdowns for entities in the processor. Considering + * how often this processor runs, caching can help move some read traffic off of the linguist DB. + * + * If this caching is using up too much memory, you can disable it by setting cacheTTL to 0. + */ + cacheTTL?: HumanDuration; + /** + * How many bytes must exist of a language in a repo before we consider it for adding a tag to + * the entity. This can be used if some repos have short utility scripts that may not be the primary + * language for the repo. + */ + bytesThreshold?: number; + /** + * Which linguist file types to process tags for. + */ + languageTypes?: LanguageType[]; +} + +/** + * This processor will fetch the language breakdown from the linguist API and + * add the languages to the entity as searchable tags. + * + * @public + * */ +export class LinguistTagsProcessor implements CatalogProcessor { + private logger: Logger; + private discovery: DiscoveryService; + private loggerMeta = { plugin: 'LinguistTagsProcessor' }; + private languageMap: Record = {}; + private shouldProcessEntity: ShouldProcessEntity = (entity: Entity) => { + return entity.kind === 'Component'; + }; + private cacheTTLMilliseconds: number; + private bytesThreshold = 0; + private languageTypes: LanguageType[] = ['programming']; + + getProcessorName(): string { + return 'LinguistTagsProcessor'; + } + + constructor(options: LinguistTagsProcessorOptions) { + this.logger = options.logger; + this.discovery = options.discovery; + if (options.shouldProcessEntity) { + this.shouldProcessEntity = options.shouldProcessEntity; + } + this.cacheTTLMilliseconds = durationToMilliseconds( + options.cacheTTL || { minutes: 30 }, + ); + if (options.bytesThreshold) { + this.bytesThreshold = options.bytesThreshold; + } + if (options.languageTypes) { + this.languageTypes = options.languageTypes; + } + if (options.languageMap) { + this.languageMap = options.languageMap; + } + } + + static fromConfig( + config: Config, + options: LinguistTagsProcessorOptions, + ): LinguistTagsProcessor { + const c = config.getOptionalConfig('linguist.tagsProcessor'); + if (c) { + options.bytesThreshold ??= c.getOptionalNumber('bytesThreshold'); + options.languageTypes ??= c.getOptionalStringArray( + 'languageTypes', + ) as LanguageType[]; + options.languageMap ??= c.getOptional('languageMap'); + options.cacheTTL ??= c.getOptional('cacheTTL'); + } + + return new LinguistTagsProcessor(options); + } + + /** + * Given an entity ref, fetches the language breakdown from the Linguist backend HTTP API. + * @param entityRef - stringified entity ref + * @returns The language breakdown + */ + private async getLanguagesFromLinguistAPI( + entityRef: string, + ): Promise { + this.logger.debug(`Fetching languages from linguist API`, { + ...this.loggerMeta, + entityRef, + }); + + const baseUrl = await this.discovery.getBaseUrl('linguist'); + const linguistApi = new URL(`${baseUrl}/entity-languages`); + linguistApi.searchParams.append('entityRef', entityRef); + const linguistData = await fetch(linguistApi).then( + res => res.json() as Promise, + ); + if (!linguistData || !linguistData.processedDate) { + return []; + } + + return linguistData.breakdown + .filter( + b => + this.languageTypes.includes(b.type) && b.bytes > this.bytesThreshold, + ) + .map(b => b.name); + } + + /** + * Cached wrapper around getLanguagesFromLinguistAPI + * @param cache - The CatalogProcessorCache + * @param entityRef - Stringified entity references + * + * @returns List of languages + */ + private async getCachedLanguages( + cache: CatalogProcessorCache, + entityRef: string, + ): Promise { + let cachedData = (await cache.get(entityRef)) as CachedData | undefined; + if (!cachedData || this.isExpired(cachedData)) { + const languages = await this.getLanguagesFromLinguistAPI(entityRef); + cachedData = { languages, cachedTime: Date.now() }; + await cache.set(entityRef, cachedData); + } + this.logger.debug(`Fetched cached languages ${cachedData.languages}`, { + ...this.loggerMeta, + entityRef, + }); + return cachedData.languages; + } + + /** + * Determines if cached data is expired based on TTL + * + * @param cachedData - The cached data for this entity + * @returns True if data is expired + */ + private isExpired(cachedData: CachedData): boolean { + const elapsed = Date.now() - (cachedData.cachedTime || 0); + return elapsed > this.cacheTTLMilliseconds; + } + + /** + * This pre-processor will fetch linguist data for a Component and convert the language breakdown + * into entity tags which will be appended to the entity. + * + * @public + */ + async preProcessEntity( + entity: Entity, + _: any, + __: any, + ___: any, + cache: CatalogProcessorCache, + ): Promise { + if (!this.shouldProcessEntity(entity)) { + return entity; + } + const entityRef = stringifyEntityRef(entity); + this.logger.debug(`Processing ${entityRef}`, { + ...this.loggerMeta, + entityRef, + }); + + const languages = + this.cacheTTLMilliseconds > 0 + ? await this.getCachedLanguages(cache, entityRef) + : await this.getLanguagesFromLinguistAPI(entityRef); + + const tags = (entity.metadata.tags ||= []); + const originalTagCount = tags.length; + + languages.forEach(lang => { + const cleanedUpLangTag = + lang in this.languageMap ? this.languageMap[lang] : sanitizeTag(lang); + if (cleanedUpLangTag && !tags.includes(cleanedUpLangTag)) { + tags.push(cleanedUpLangTag); + } + }); + + const addedCount = tags.length - originalTagCount; + + this.logger.debug(`Added ${addedCount} language tags from linguist`, { + ...this.loggerMeta, + entityRef, + }); + + return entity; + } +} + +/** + * Converts language tags from linguist to something acceptable by + * the tag requirements for backstage + * + * @param tag - A language tag from linguist + * @returns Cleaned up language tag + * @internal + */ +export function sanitizeTag(tag: string): string { + return tag + .toLowerCase() + .replace(/\.net/g, '-dot-net') + .replace(/[^a-z0-9:+#-]+/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^-+|-+$/g, ''); +} diff --git a/plugins/linguist-backend/src/processor/__snapshots__/LinguistTagsProcessor.test.ts.snap b/plugins/linguist-backend/src/processor/__snapshots__/LinguistTagsProcessor.test.ts.snap new file mode 100644 index 0000000000..4eb428f61a --- /dev/null +++ b/plugins/linguist-backend/src/processor/__snapshots__/LinguistTagsProcessor.test.ts.snap @@ -0,0 +1,707 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sanitizeTag Should clean up all linguist languages 1`] = ` +[ + "1c-enterprise", + "2-dimensional-array", + "4d", + "abap", + "abap-cds", + "abnf", + "ags-script", + "aidl", + "al", + "ampl", + "antlr", + "api-blueprint", + "apl", + "asl", + "asn-1", + "asp-dot-net", + "ats", + "actionscript", + "ada", + "adblock-filter-list", + "adobe-font-metrics", + "agda", + "alloy", + "alpine-abuild", + "altium-designer", + "angelscript", + "ant-build-system", + "antlers", + "apacheconf", + "apex", + "apollo-guidance-computer", + "applescript", + "arc", + "asciidoc", + "aspectj", + "assembly", + "astro", + "asymptote", + "augeas", + "autohotkey", + "autoit", + "avro-idl", + "awk", + "basic", + "ballerina", + "batchfile", + "beef", + "befunge", + "berry", + "bibtex", + "bicep", + "bikeshed", + "bison", + "bitbake", + "blade", + "blitzbasic", + "blitzmax", + "bluespec", + "boo", + "boogie", + "brainfuck", + "brighterscript", + "brightscript", + "browserslist", + "c", + "c#", + "c++", + "c-objdump", + "c2hs-haskell", + "cap-cds", + "cil", + "clips", + "cmake", + "cobol", + "codeowners", + "collada", + "cson", + "css", + "csv", + "cue", + "cweb", + "cabal-config", + "cadence", + "cairo", + "cameligo", + "cap-n-proto", + "cartocss", + "ceylon", + "chapel", + "charity", + "checksums", + "chuck", + "circom", + "cirru", + "clarion", + "clarity", + "classic-asp", + "clean", + "click", + "clojure", + "closure-templates", + "cloud-firestore-security-rules", + "conll-u", + "codeql", + "coffeescript", + "coldfusion", + "coldfusion-cfc", + "common-lisp", + "common-workflow-language", + "component-pascal", + "cool", + "coq", + "cpp-objdump", + "creole", + "crystal", + "csound", + "csound-document", + "csound-score", + "cuda", + "cue-sheet", + "curry", + "cycript", + "cypher", + "cython", + "d", + "d-objdump", + "d2", + "digital-command-language", + "dm", + "dns-zone", + "dtrace", + "dafny", + "darcs-patch", + "dart", + "dataweave", + "debian-package-control-file", + "denizenscript", + "dhall", + "diff", + "directx-3d-file", + "dockerfile", + "dogescript", + "dotenv", + "dylan", + "e", + "e-mail", + "ebnf", + "ecl", + "eclipse", + "ejs", + "eq", + "eagle", + "earthly", + "easybuild", + "ecere-projects", + "ecmarkup", + "editorconfig", + "edje-data-collection", + "eiffel", + "elixir", + "elm", + "elvish", + "elvish-transcript", + "emacs-lisp", + "emberscript", + "erlang", + "euphoria", + "f#", + "f", + "figlet-font", + "flux", + "factor", + "fancy", + "fantom", + "faust", + "fennel", + "filebench-wml", + "filterscript", + "fluent", + "formatted", + "forth", + "fortran", + "fortran-free-form", + "freebasic", + "freemarker", + "frege", + "futhark", + "g-code", + "gaml", + "gams", + "gap", + "gcc-machine-description", + "gdb", + "gdscript", + "gedcom", + "glsl", + "gn", + "gsc", + "game-maker-language", + "gemfile-lock", + "gemini", + "genero", + "genero-forms", + "genie", + "genshi", + "gentoo-ebuild", + "gentoo-eclass", + "gerber-image", + "gettext-catalog", + "gherkin", + "git-attributes", + "git-config", + "git-revision-list", + "gleam", + "glyph", + "glyph-bitmap-distribution-format", + "gnuplot", + "go", + "go-checksums", + "go-module", + "go-workspace", + "godot-resource", + "golo", + "gosu", + "grace", + "gradle", + "grammatical-framework", + "graph-modeling-language", + "graphql", + "graphviz-dot", + "groovy", + "groovy-server-pages", + "haproxy", + "hcl", + "hlsl", + "hocon", + "html", + "html+ecr", + "html+eex", + "html+erb", + "html+php", + "html+razor", + "http", + "hxml", + "hack", + "haml", + "handlebars", + "harbour", + "haskell", + "haxe", + "hiveql", + "holyc", + "hosts-file", + "hy", + "hyphy", + "idl", + "igor-pro", + "ini", + "irc-log", + "idris", + "ignore-list", + "imagej-macro", + "imba", + "inform-7", + "ink", + "inno-setup", + "io", + "ioke", + "isabelle", + "isabelle-root", + "j", + "jar-manifest", + "jcl", + "jflex", + "json", + "json-with-comments", + "json5", + "jsonld", + "jsoniq", + "janet", + "jasmin", + "java", + "java-properties", + "java-server-pages", + "javascript", + "javascript+erb", + "jest-snapshot", + "jetbrains-mps", + "jinja", + "jison", + "jison-lex", + "jolie", + "jsonnet", + "julia", + "jupyter-notebook", + "just", + "krl", + "kaitai-struct", + "kakounescript", + "kerboscript", + "kicad-layout", + "kicad-legacy-layout", + "kicad-schematic", + "kickstart", + "kit", + "kotlin", + "kusto", + "lfe", + "llvm", + "lolcode", + "lsl", + "ltspice-symbol", + "labview", + "lark", + "lasso", + "latte", + "lean", + "less", + "lex", + "ligolang", + "lilypond", + "limbo", + "linker-script", + "linux-kernel-module", + "liquid", + "literate-agda", + "literate-coffeescript", + "literate-haskell", + "livescript", + "logos", + "logtalk", + "lookml", + "loomscript", + "lua", + "m", + "m4", + "m4sugar", + "matlab", + "maxscript", + "mdx", + "mlir", + "mql4", + "mql5", + "mtml", + "muf", + "macaulay2", + "makefile", + "mako", + "markdown", + "marko", + "mask", + "mathematica", + "maven-pom", + "max", + "mercury", + "mermaid", + "meson", + "metal", + "microsoft-developer-studio-project", + "microsoft-visual-studio-solution", + "minid", + "miniyaml", + "mint", + "mirah", + "modelica", + "modula-2", + "modula-3", + "module-management-system", + "monkey", + "monkey-c", + "moocode", + "moonscript", + "motoko", + "motorola-68k-assembly", + "move", + "muse", + "mustache", + "myghty", + "nasl", + "ncl", + "neon", + "nl", + "npm-config", + "nsis", + "nwscript", + "nasal", + "nearley", + "nemerle", + "netlinx", + "netlinx+erb", + "netlogo", + "newlisp", + "nextflow", + "nginx", + "nim", + "ninja", + "nit", + "nix", + "nu", + "numpy", + "nunjucks", + "nushell", + "oasv2-json", + "oasv2-yaml", + "oasv3-json", + "oasv3-yaml", + "ocaml", + "objdump", + "object-data-instance-notation", + "objectscript", + "objective-c", + "objective-c++", + "objective-j", + "odin", + "omgrofl", + "opa", + "opal", + "open-policy-agent", + "openapi-specification-v2", + "openapi-specification-v3", + "opencl", + "openedge-abl", + "openqasm", + "openrc-runscript", + "openscad", + "openstep-property-list", + "opentype-feature-file", + "option-list", + "org", + "ox", + "oxygene", + "oz", + "p4", + "pddl", + "peg-js", + "php", + "plsql", + "plpgsql", + "pov-ray-sdl", + "pact", + "pan", + "papyrus", + "parrot", + "parrot-assembly", + "parrot-internal-representation", + "pascal", + "pawn", + "pep8", + "perl", + "pic", + "pickle", + "picolisp", + "piglatin", + "pike", + "plantuml", + "pod", + "pod-6", + "pogoscript", + "polar", + "pony", + "portugol", + "postcss", + "postscript", + "powerbuilder", + "powershell", + "prisma", + "processing", + "procfile", + "proguard", + "prolog", + "promela", + "propeller-spin", + "protocol-buffer", + "protocol-buffer-text-format", + "public-key", + "pug", + "puppet", + "pure-data", + "purebasic", + "purescript", + "pyret", + "python", + "python-console", + "python-traceback", + "q#", + "qml", + "qmake", + "qt-script", + "quake", + "r", + "raml", + "rbs", + "rdoc", + "realbasic", + "rexx", + "rmarkdown", + "rpc", + "rpgle", + "rpm-spec", + "runoff", + "racket", + "ragel", + "raku", + "rascal", + "raw-token-data", + "rescript", + "readline-config", + "reason", + "reasonligo", + "rebol", + "record-jar", + "red", + "redcode", + "redirect-rules", + "regular-expression", + "ren-py", + "renderscript", + "rich-text-format", + "ring", + "riot", + "robotframework", + "roff", + "roff-manpage", + "rouge", + "routeros-script", + "ruby", + "rust", + "sas", + "scss", + "selinux-policy", + "smt", + "sparql", + "sqf", + "sql", + "sqlpl", + "srecode-template", + "ssh-config", + "star", + "stl", + "ston", + "svg", + "swig", + "sage", + "saltstack", + "sass", + "scala", + "scaml", + "scenic", + "scheme", + "scilab", + "self", + "shaderlab", + "shell", + "shellcheck-config", + "shellsession", + "shen", + "sieve", + "simple-file-verification", + "singularity", + "slash", + "slice", + "slim", + "smpl", + "smali", + "smalltalk", + "smarty", + "smithy", + "snakemake", + "solidity", + "soong", + "sourcepawn", + "spline-font-database", + "squirrel", + "stan", + "standard-ml", + "starlark", + "stata", + "stringtemplate", + "stylus", + "subrip-text", + "sugarss", + "supercollider", + "svelte", + "sway", + "swift", + "systemverilog", + "ti-program", + "tl-verilog", + "tla", + "toml", + "tsql", + "tsv", + "tsx", + "txl", + "talon", + "tcl", + "tcsh", + "tex", + "tea", + "terra", + "texinfo", + "text", + "textmate-properties", + "textile", + "thrift", + "turing", + "turtle", + "twig", + "type-language", + "typescript", + "unified-parallel-c", + "unity3d-asset", + "unix-assembly", + "uno", + "unrealscript", + "urweb", + "v", + "vba", + "vbscript", + "vcl", + "vhdl", + "vala", + "valve-data-format", + "velocity-template-language", + "verilog", + "vim-help-file", + "vim-script", + "vim-snippet", + "visual-basic-dot-net", + "visual-basic-6-0", + "volt", + "vue", + "vyper", + "wdl", + "wgsl", + "wavefront-material", + "wavefront-object", + "web-ontology-language", + "webassembly", + "webassembly-interface-type", + "webidl", + "webvtt", + "wget-config", + "whiley", + "wikitext", + "win32-message-file", + "windows-registry-entries", + "witcher-script", + "wollok", + "world-of-warcraft-addon-data", + "wren", + "x-bitmap", + "x-font-directory-index", + "x-pixmap", + "x10", + "xc", + "xcompose", + "xml", + "xml-property-list", + "xpages", + "xproc", + "xquery", + "xs", + "xslt", + "xojo", + "xonsh", + "xtend", + "yaml", + "yang", + "yara", + "yasnippet", + "yacc", + "yul", + "zap", + "zil", + "zeek", + "zenscript", + "zephir", + "zig", + "zimpl", + "curl-config", + "desktop", + "dircolors", + "ec", + "edn", + "fish", + "hoon", + "jq", + "kvlang", + "mirc-script", + "mcfunction", + "mupad", + "nanorc", + "nesc", + "ooc", + "q", + "restructuredtext", + "robots-txt", + "sed", + "wisp", + "xbase", +] +`; diff --git a/plugins/linguist-backend/src/processor/index.ts b/plugins/linguist-backend/src/processor/index.ts new file mode 100644 index 0000000000..b56618b203 --- /dev/null +++ b/plugins/linguist-backend/src/processor/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type { + LinguistTagsProcessorOptions, + ShouldProcessEntity, +} from './LinguistTagsProcessor'; +export { LinguistTagsProcessor } from './LinguistTagsProcessor'; diff --git a/plugins/linguist-common/api-report.md b/plugins/linguist-common/api-report.md index 954bef7904..9ab81fd41d 100644 --- a/plugins/linguist-common/api-report.md +++ b/plugins/linguist-common/api-report.md @@ -23,7 +23,7 @@ export type Language = { name: string; percentage: number; bytes: number; - type: string; + type: LanguageType; color?: `#${string}`; }; @@ -35,6 +35,9 @@ export type Languages = { breakdown: Language[]; }; +// @public (undocumented) +export type LanguageType = 'programming' | 'data' | 'markup' | 'prose'; + // @public (undocumented) export const LINGUIST_ANNOTATION = 'backstage.io/linguist'; diff --git a/plugins/linguist-common/src/types.ts b/plugins/linguist-common/src/types.ts index 28a25ed059..515ca45f9c 100644 --- a/plugins/linguist-common/src/types.ts +++ b/plugins/linguist-common/src/types.ts @@ -28,12 +28,15 @@ export type Languages = { breakdown: Language[]; }; +/** @public */ +export type LanguageType = 'programming' | 'data' | 'markup' | 'prose'; + /** @public */ export type Language = { name: string; percentage: number; bytes: number; - type: string; + type: LanguageType; color?: `#${string}`; }; diff --git a/yarn.lock b/yarn.lock index bb795ca564..c8ecd80abc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7734,6 +7734,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-linguist-common": "workspace:^" "@backstage/types": "workspace:^" "@types/express": "*" @@ -7741,6 +7742,7 @@ __metadata: express: ^4.18.1 express-promise-router: ^4.1.0 fs-extra: ^10.0.0 + js-yaml: ^4.1.0 knex: ^2.0.0 linguist-js: ^2.5.3 luxon: ^2.0.2 From a4b364ec146a9d19fb65780bbd2b79678fcbf17d Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 14:32:14 +0200 Subject: [PATCH 013/329] Add some initial code Signed-off-by: Jonathan Mezach --- .github/CODEOWNERS | 176 +++++++++--------- packages/app/package.json | 1 + plugins/analytics-module-nr/.eslintrc.js | 1 + plugins/analytics-module-nr/README.md | 13 ++ plugins/analytics-module-nr/config.d.ts | 59 ++++++ .../analytics-module-nr/dev/Playground.tsx | 26 +++ plugins/analytics-module-nr/dev/index.tsx | 51 +++++ plugins/analytics-module-nr/package.json | 55 ++++++ .../AnalyticsApi/NewRelicBrowser.ts | 110 +++++++++++ .../implementations/AnalyticsApi/index.ts | 16 ++ plugins/analytics-module-nr/src/index.ts | 16 ++ plugins/analytics-module-nr/src/setupTests.ts | 16 ++ yarn.lock | 132 ++++++++++++- 13 files changed, 583 insertions(+), 89 deletions(-) create mode 100644 plugins/analytics-module-nr/.eslintrc.js create mode 100644 plugins/analytics-module-nr/README.md create mode 100644 plugins/analytics-module-nr/config.d.ts create mode 100644 plugins/analytics-module-nr/dev/Playground.tsx create mode 100644 plugins/analytics-module-nr/dev/index.tsx create mode 100644 plugins/analytics-module-nr/package.json create mode 100644 plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts create mode 100644 plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/index.ts create mode 100644 plugins/analytics-module-nr/src/index.ts create mode 100644 plugins/analytics-module-nr/src/setupTests.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 82a9eb499d..ea5b656bf7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,91 +4,91 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @backstage/maintainers -yarn.lock @backstage/maintainers @backstage-service -*/yarn.lock @backstage/maintainers @backstage-service -/.changeset/*.md -/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-maintainers -/docs/assets/search @backstage/discoverability-maintainers -/docs/features/search @backstage/discoverability-maintainers -/docs/features/techdocs @backstage/techdocs-maintainers -/docs/plugins/integrating-search-into-plugins.md @backstage/discoverability-maintainers -/packages/cli/src/commands/onboard @backstage/sharks -/packages/techdocs-cli @backstage/techdocs-maintainers -/packages/techdocs-cli-embedded-app @backstage/techdocs-maintainers -/plugins/adr @backstage/maintainers @kuangp -/plugins/adr-* @backstage/maintainers @kuangp -/plugins/allure @backstage/maintainers @deepak-bhardwaj-ps -/plugins/apache-airflow @backstage/maintainers @cmpadden -/plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers -/plugins/azure-devops @backstage/maintainers @marleypowell @awanlin -/plugins/azure-devops-backend @backstage/maintainers @marleypowell @awanlin -/plugins/azure-devops-common @backstage/maintainers @marleypowell @awanlin -/plugins/bitbucket-cloud-common @backstage/maintainers @pjungermann -/plugins/bitrise @backstage/maintainers @backstage/sda-se-reviewers -/plugins/catalog @backstage/maintainers @backstage/catalog-maintainers -/plugins/catalog-* @backstage/maintainers @backstage/catalog-maintainers -/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/catalog-maintainers @tdabasinskas -/plugins/catalog-graph @backstage/maintainers @backstage/catalog-maintainers @backstage/sda-se-reviewers -/plugins/circleci @backstage/maintainers @adamdmharvey -/plugins/cloudbuild @backstage/maintainers @trivago/ebarrios -/plugins/code-coverage @backstage/maintainers @alde @nissayeva -/plugins/code-coverage-backend @backstage/maintainers @alde @nissayeva -/plugins/cost-insights @backstage/maintainers @backstage/silver-lining -/plugins/cost-insights-* @backstage/maintainers @backstage/silver-lining -/plugins/devtools @backstage/maintainers @awanlin -/plugins/devtools-backend @backstage/maintainers @awanlin -/plugins/devtools-common @backstage/maintainers @awanlin -/plugins/entity-feedback @backstage/maintainers @kuangp -/plugins/entity-feedback-* @backstage/maintainers @kuangp -/plugins/events-backend @backstage/maintainers @pjungermann -/plugins/events-backend-module-aws-sqs @backstage/maintainers @pjungermann -/plugins/events-backend-module-azure @backstage/maintainers @pjungermann -/plugins/events-backend-module-bitbucket-cloud @backstage/maintainers @pjungermann -/plugins/events-backend-module-gerrit @backstage/maintainers @pjungermann -/plugins/events-backend-module-github @backstage/maintainers @pjungermann -/plugins/events-backend-module-gitlab @backstage/maintainers @pjungermann -/plugins/events-backend-test-utils @backstage/maintainers @pjungermann -/plugins/events-node @backstage/maintainers @pjungermann -/plugins/explore @backstage/maintainers @backstage/sda-se-reviewers -/plugins/explore-react @backstage/maintainers @backstage/sda-se-reviewers -/plugins/fossa @backstage/maintainers @backstage/sda-se-reviewers -/plugins/gcalendar @backstage/maintainers @szubster @ptychu @kielosz @alexrybch -/plugins/git-release-manager @backstage/maintainers @erikengervall -/plugins/home @backstage/discoverability-maintainers -/plugins/home-* @backstage/discoverability-maintainers -/plugins/ilert @backstage/maintainers @yacut -/plugins/jenkins @backstage/maintainers @timja -/plugins/jenkins-backend @backstage/maintainers @timja -/plugins/kafka @backstage/maintainers @nirga @andrewthauer -/plugins/kafka-backend @backstage/maintainers @nirga @andrewthauer -/plugins/kubernetes @backstage/maintainers @backstage/kubernetes-maintainers -/plugins/kubernetes-* @backstage/maintainers @backstage/kubernetes-maintainers -/plugins/linguist @backstage/maintainers @awanlin -/plugins/linguist-backend @backstage/maintainers @awanlin -/plugins/linguist-common @backstage/maintainers @awanlin -/plugins/microsoft-calendar @backstage/maintainers @abhay-soni-developer @NishkarshRaj -/plugins/newrelic-dashboard @backstage/maintainers @mufaddal7 -/plugins/permission-* @backstage/permission-maintainers -/plugins/playlist @backstage/maintainers @kuangp -/plugins/playlist-* @backstage/maintainers @kuangp -/plugins/puppetdb @backstage/maintainers @tdabasinskas -/plugins/rollbar @backstage/maintainers @andrewthauer -/plugins/rollbar-backend @backstage/maintainers @andrewthauer -/plugins/scaffolder-backend-module-rails @backstage/maintainers @angeliski -/plugins/scaffolder-backend-module-yeoman @backstage/maintainers @pawelmitka -/plugins/search @backstage/discoverability-maintainers -/plugins/search-* @backstage/discoverability-maintainers -/plugins/sonarqube @backstage/maintainers @backstage/sda-se-reviewers -/plugins/stack-overflow @backstage/discoverability-maintainers -/plugins/stack-overflow-backend @backstage/discoverability-maintainers -/plugins/techdocs @backstage/techdocs-maintainers -/plugins/techdocs-* @backstage/techdocs-maintainers -/plugins/user-settings-backend @backstage/maintainers @backstage/sda-se-reviewers -/tech-insights-backend @backstage/maintainers @xantier @iain-b -/tech-insights-backend-module-jsonfc @backstage/maintainers @xantier @iain-b -/tech-insights-tech-insights-common @backstage/maintainers @xantier @iain-b -/tech-insights-tech-insights-node @backstage/maintainers @xantier @iain-b +* @backstage/maintainers +*/yarn.lock @backstage/maintainers @backstage-service +/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-maintainers +/docs/assets/search @backstage/discoverability-maintainers +/docs/features/search @backstage/discoverability-maintainers +/docs/features/techdocs @backstage/techdocs-maintainers +/docs/plugins/integrating-search-into-plugins.md @backstage/discoverability-maintainers +/packages/cli/src/commands/onboard @backstage/sharks +/packages/techdocs-cli @backstage/techdocs-maintainers +/packages/techdocs-cli-embedded-app @backstage/techdocs-maintainers +/plugins/adr @backstage/maintainers @kuangp +/plugins/adr-* @backstage/maintainers @kuangp +/plugins/allure @backstage/maintainers @deepak-bhardwaj-ps +/plugins/analytics-module-nr @jmezach +/plugins/apache-airflow @backstage/maintainers @cmpadden +/plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers +/plugins/azure-devops @backstage/maintainers @marleypowell @awanlin +/plugins/azure-devops-backend @backstage/maintainers @marleypowell @awanlin +/plugins/azure-devops-common @backstage/maintainers @marleypowell @awanlin +/plugins/bitbucket-cloud-common @backstage/maintainers @pjungermann +/plugins/bitrise @backstage/maintainers @backstage/sda-se-reviewers +/plugins/catalog @backstage/maintainers @backstage/catalog-maintainers +/plugins/catalog-* @backstage/maintainers @backstage/catalog-maintainers +/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/catalog-maintainers @tdabasinskas +/plugins/catalog-graph @backstage/maintainers @backstage/catalog-maintainers @backstage/sda-se-reviewers +/plugins/circleci @backstage/maintainers @adamdmharvey +/plugins/cloudbuild @backstage/maintainers @trivago/ebarrios +/plugins/code-coverage @backstage/maintainers @alde @nissayeva +/plugins/code-coverage-backend @backstage/maintainers @alde @nissayeva +/plugins/cost-insights @backstage/maintainers @backstage/silver-lining +/plugins/cost-insights-* @backstage/maintainers @backstage/silver-lining +/plugins/devtools @backstage/maintainers @awanlin +/plugins/devtools-backend @backstage/maintainers @awanlin +/plugins/devtools-common @backstage/maintainers @awanlin +/plugins/entity-feedback @backstage/maintainers @kuangp +/plugins/entity-feedback-* @backstage/maintainers @kuangp +/plugins/events-backend @backstage/maintainers @pjungermann +/plugins/events-backend-module-aws-sqs @backstage/maintainers @pjungermann +/plugins/events-backend-module-azure @backstage/maintainers @pjungermann +/plugins/events-backend-module-bitbucket-cloud @backstage/maintainers @pjungermann +/plugins/events-backend-module-gerrit @backstage/maintainers @pjungermann +/plugins/events-backend-module-github @backstage/maintainers @pjungermann +/plugins/events-backend-module-gitlab @backstage/maintainers @pjungermann +/plugins/events-backend-test-utils @backstage/maintainers @pjungermann +/plugins/events-node @backstage/maintainers @pjungermann +/plugins/explore @backstage/maintainers @backstage/sda-se-reviewers +/plugins/explore-react @backstage/maintainers @backstage/sda-se-reviewers +/plugins/fossa @backstage/maintainers @backstage/sda-se-reviewers +/plugins/gcalendar @backstage/maintainers @szubster @ptychu @kielosz @alexrybch +/plugins/git-release-manager @backstage/maintainers @erikengervall +/plugins/home @backstage/discoverability-maintainers +/plugins/home-* @backstage/discoverability-maintainers +/plugins/ilert @backstage/maintainers @yacut +/plugins/jenkins @backstage/maintainers @timja +/plugins/jenkins-backend @backstage/maintainers @timja +/plugins/kafka @backstage/maintainers @nirga @andrewthauer +/plugins/kafka-backend @backstage/maintainers @nirga @andrewthauer +/plugins/kubernetes @backstage/maintainers @backstage/kubernetes-maintainers +/plugins/kubernetes-* @backstage/maintainers @backstage/kubernetes-maintainers +/plugins/linguist @backstage/maintainers @awanlin +/plugins/linguist-backend @backstage/maintainers @awanlin +/plugins/linguist-common @backstage/maintainers @awanlin +/plugins/microsoft-calendar @backstage/maintainers @abhay-soni-developer @NishkarshRaj +/plugins/newrelic-dashboard @backstage/maintainers @mufaddal7 +/plugins/permission-* @backstage/permission-maintainers +/plugins/playlist @backstage/maintainers @kuangp +/plugins/playlist-* @backstage/maintainers @kuangp +/plugins/puppetdb @backstage/maintainers @tdabasinskas +/plugins/rollbar @backstage/maintainers @andrewthauer +/plugins/rollbar-backend @backstage/maintainers @andrewthauer +/plugins/scaffolder-backend-module-rails @backstage/maintainers @angeliski +/plugins/scaffolder-backend-module-yeoman @backstage/maintainers @pawelmitka +/plugins/search @backstage/discoverability-maintainers +/plugins/search-* @backstage/discoverability-maintainers +/plugins/sonarqube @backstage/maintainers @backstage/sda-se-reviewers +/plugins/stack-overflow @backstage/discoverability-maintainers +/plugins/stack-overflow-backend @backstage/discoverability-maintainers +/plugins/techdocs @backstage/techdocs-maintainers +/plugins/techdocs-* @backstage/techdocs-maintainers +/plugins/user-settings-backend @backstage/maintainers @backstage/sda-se-reviewers +/tech-insights-backend @backstage/maintainers @xantier @iain-b +/tech-insights-backend-module-jsonfc @backstage/maintainers @xantier @iain-b +/tech-insights-tech-insights-common @backstage/maintainers @xantier @iain-b +/tech-insights-tech-insights-node @backstage/maintainers @xantier @iain-b +yarn.lock @backstage/maintainers @backstage-service diff --git a/packages/app/package.json b/packages/app/package.json index 0f3a65c26a..10482ffeb4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -85,6 +85,7 @@ "@roadiehq/backstage-plugin-github-insights": "^2.0.5", "@roadiehq/backstage-plugin-github-pull-requests": "^2.2.7", "@roadiehq/backstage-plugin-travis-ci": "^2.0.5", + "backstage-plugin-analytics-module-nr": "^0.0.0", "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^17.0.2", diff --git a/plugins/analytics-module-nr/.eslintrc.js b/plugins/analytics-module-nr/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/analytics-module-nr/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/analytics-module-nr/README.md b/plugins/analytics-module-nr/README.md new file mode 100644 index 0000000000..131a7d9c63 --- /dev/null +++ b/plugins/analytics-module-nr/README.md @@ -0,0 +1,13 @@ +# Analytics Module: New Relic Browser + +This plugin provides an opinionated implementation of the Backstage Analytics API for New Relic Browser. Once installed and configured, analytics events will be sent to New Relic as your users navigate and use your Backstage instance. + +This plugin contains no other functionality. + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/analytics-module-nr](http://localhost:3000/analytics-module-nr). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/analytics-module-nr/config.d.ts b/plugins/analytics-module-nr/config.d.ts new file mode 100644 index 0000000000..e9c1b3ebca --- /dev/null +++ b/plugins/analytics-module-nr/config.d.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + app: { + analytics?: { + nr: { + /** + * New Relic Account ID, e.g. 1234567 + * @visibility frontend + */ + accountId: string; + + /** + * New Relic Application ID, e.g. 987654321 + * @visibility frontend + */ + applicationId: string; + + /** + * New Relic License Key, e.g. NRJS-12a3456bc78de9123f4 + * @visibility frontend + */ + licenseKey: string; + + /** + * Whether to enabled distributed tracing, defaults to false + * @visibility frontend + */ + distributedTracingEnabled: boolean; + + /** + * Whether to enabled tracing of cookies, defaults to false + * @visibility frontend + */ + cookiesEnabled: boolean; + + /** + * Whether to use New Relic's EU Datacenter endpoints, defaults to false + * @visibility frontend + */ + useEuEndpoint: boolean; + }; + }; + }; +} diff --git a/plugins/analytics-module-nr/dev/Playground.tsx b/plugins/analytics-module-nr/dev/Playground.tsx new file mode 100644 index 0000000000..cf21aadacc --- /dev/null +++ b/plugins/analytics-module-nr/dev/Playground.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Link } from '@backstage/core-components'; + +export const Playground = () => { + return ( + <> + Click Here + + ); +}; diff --git a/plugins/analytics-module-nr/dev/index.tsx b/plugins/analytics-module-nr/dev/index.tsx new file mode 100644 index 0000000000..796bade4a9 --- /dev/null +++ b/plugins/analytics-module-nr/dev/index.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { + analyticsApiRef, + configApiRef, + createPlugin, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { Playground } from '../../analytics-module-nr/dev/Playground'; +import { NewRelicBrowser } from '../src'; + +/** + * @deprecated Importing and including this plugin in an app has no effect. + * This will be removed in a future release. + * + * @public + */ +export const analyticsModuleNR = createPlugin({ + id: 'analytics-provider-nr', +}); +createDevApp() + .registerPlugin(analyticsModuleNR) + .registerApi({ + api: analyticsApiRef, + deps: { configApi: configApiRef, identityApi: identityApiRef }, + factory: ({ configApi, identityApi }) => + NewRelicBrowser.fromConfig(configApi, { + identityApi, + }), + }) + .addPage({ + path: '/nr', + title: 'New Relic Playground', + element: , + }) + .render(); diff --git a/plugins/analytics-module-nr/package.json b/plugins/analytics-module-nr/package.json new file mode 100644 index 0000000000..3cc446150a --- /dev/null +++ b/plugins/analytics-module-nr/package.json @@ -0,0 +1,55 @@ +{ + "name": "backstage-plugin-analytics-module-nr", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/config": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.60", + "@newrelic/browser-agent": "^1.236.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "msw": "^1.0.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts new file mode 100644 index 0000000000..5270dce156 --- /dev/null +++ b/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Config } from '@backstage/config'; +import { + AnalyticsApi, + IdentityApi, + AnalyticsEvent, +} from '@backstage/core-plugin-api'; +import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent'; +import type { setAPI } from '@newrelic/browser-agent/loaders/api/api'; + +type NewRelicAPI = ReturnType; + +export type NewRelicBrowserOptions = { + accountId: string; + applicationId: string; + licenseKey: string; + distributedTracingEnabled: boolean; + cookiesEnabled: boolean; + useEuEndpoint: boolean; +}; + +// Implementation that optionally initializes with a userId. +export class NewRelicBrowser implements AnalyticsApi { + private readonly agent: NewRelicAPI; + + private constructor( + options: NewRelicBrowserOptions, + identityApi?: IdentityApi, + ) { + // Configure the New Relic Browser agent + const agentOptions = { + init: { + distributed_tracing: { + enabled: options.distributedTracingEnabled, + }, + privacy: { + cookies_enabled: options.cookiesEnabled, + }, + ajax: { + deny_list: [ + options.useEuEndpoint ? 'bam.eu01.nr-data.net' : 'bam.nr-data.net', + ], + }, + }, + info: { + beacon: options.useEuEndpoint + ? 'bam.eu01.nr-data.net' + : 'bam.nr-data.net', + errorBeacon: options.useEuEndpoint + ? 'bam.eu01.nr-data.net' + : 'bam.nr-data.net', + licenseKey: options.licenseKey, + applicationID: options.applicationId, + sa: 1, + }, + loader_config: { + accountID: options.accountId, + trustKey: options.accountId, + agentID: options.applicationId, + licenseKey: options.licenseKey, + applicationID: options.applicationId, + }, + }; + + // Initialize the agent + this.agent = new BrowserAgent(agentOptions) as unknown as NewRelicAPI; + + if (identityApi) { + identityApi.getBackstageIdentity().then(identity => { + this.agent.setUserId(identity.userEntityRef); + }); + } + } + + static fromConfig(config: Config, options: { identityApi?: IdentityApi }) { + const browserOptions: NewRelicBrowserOptions = { + accountId: config.getString('app.analytics.nr.accountId'), + applicationId: config.getString('app.analytics.nr.applicationId'), + licenseKey: config.getString('app.analytics.nr.licenseKey'), + distributedTracingEnabled: + config.getOptionalBoolean( + 'app.analytics.nr.distributedTracingEnabled', + ) ?? false, + cookiesEnabled: + config.getOptionalBoolean('app.analytics.nr.cookiesEnabled') ?? false, + useEuEndpoint: + config.getOptionalBoolean('app.analytics.nr.useEuEndpoint') ?? false, + }; + return new NewRelicBrowser(browserOptions, options.identityApi); + } + + captureEvent(event: AnalyticsEvent) { + const { action, ...rest } = event; + this.agent.addPageAction(action, rest); + } +} diff --git a/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/index.ts b/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/index.ts new file mode 100644 index 0000000000..6300381cfd --- /dev/null +++ b/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { NewRelicBrowser } from './NewRelicBrowser'; diff --git a/plugins/analytics-module-nr/src/index.ts b/plugins/analytics-module-nr/src/index.ts new file mode 100644 index 0000000000..0adf114679 --- /dev/null +++ b/plugins/analytics-module-nr/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './apis/implementations/AnalyticsApi'; diff --git a/plugins/analytics-module-nr/src/setupTests.ts b/plugins/analytics-module-nr/src/setupTests.ts new file mode 100644 index 0000000000..865308e634 --- /dev/null +++ b/plugins/analytics-module-nr/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; diff --git a/yarn.lock b/yarn.lock index c801c6440b..96b1aefaea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13535,6 +13535,18 @@ __metadata: languageName: node linkType: hard +"@newrelic/browser-agent@npm:^1.236.0": + version: 1.236.0 + resolution: "@newrelic/browser-agent@npm:1.236.0" + dependencies: + core-js: ^3.26.0 + fflate: ^0.7.4 + rrweb: ^2.0.0-alpha.8 + web-vitals: ^3.1.0 + checksum: 79a96c8f8421bed13e0e9eaf08c9bb8ac2785da3199e2304366c5cf98eb976713e393bd26a7efe998837eba5facfe7a54a6d58ba5c948c7ecc8de49e477b5836 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -14856,6 +14868,15 @@ __metadata: languageName: node linkType: hard +"@rrweb/types@npm:^2.0.0-alpha.9": + version: 2.0.0-alpha.9 + resolution: "@rrweb/types@npm:2.0.0-alpha.9" + dependencies: + rrweb-snapshot: ^2.0.0-alpha.9 + checksum: adc6bc7a6e45294ae7b85a137ae6774a822f103f0a29ecf8d59b72c88b6ddfaedc5a16d71971c08a2ab25e3e3af89f9582d8508bcf6b33d97fc326cdf91a09b2 + languageName: node + linkType: hard + "@rushstack/node-core-library@npm:3.53.3": version: 3.53.3 resolution: "@rushstack/node-core-library@npm:3.53.3" @@ -16592,6 +16613,13 @@ __metadata: languageName: node linkType: hard +"@types/css-font-loading-module@npm:0.0.7": + version: 0.0.7 + resolution: "@types/css-font-loading-module@npm:0.0.7" + checksum: a074d3b824b2232160f2353daf1cc62937e4a24154d13607f14f93feaaac6abcf069c8ae02c1396840950fa02c1b3f19ce0f119321f9735905bfb3cd4b9b2021 + languageName: node + linkType: hard + "@types/d3-array@npm:^3.0.3": version: 3.0.4 resolution: "@types/d3-array@npm:3.0.4" @@ -18728,6 +18756,13 @@ __metadata: languageName: node linkType: hard +"@xstate/fsm@npm:^1.4.0": + version: 1.6.5 + resolution: "@xstate/fsm@npm:1.6.5" + checksum: da24fa4f40479223da34714640cea64ec894bbfeb2281a0a870af68882ae1d6fb6d724d6e5c0d4e6929728c699b92858f7389ef7d6a5d00ac66ba8cde3934eed + languageName: node + linkType: hard + "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -19893,6 +19928,33 @@ __metadata: languageName: node linkType: hard +"backstage-plugin-analytics-module-nr@^0.0.0, backstage-plugin-analytics-module-nr@workspace:plugins/analytics-module-nr": + version: 0.0.0-use.local + resolution: "backstage-plugin-analytics-module-nr@workspace:plugins/analytics-module-nr" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.60 + "@newrelic/browser-agent": ^1.236.0 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + msw: ^1.0.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "badge-maker@npm:^3.3.0": version: 3.3.1 resolution: "badge-maker@npm:3.3.1" @@ -19926,6 +19988,13 @@ __metadata: languageName: node linkType: hard +"base64-arraybuffer@npm:^1.0.1": + version: 1.0.2 + resolution: "base64-arraybuffer@npm:1.0.2" + checksum: 15e6400d2d028bf18be4ed97702b11418f8f8779fb8c743251c863b726638d52f69571d4cc1843224da7838abef0949c670bde46936663c45ad078e89fee5c62 + languageName: node + linkType: hard + "base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -21898,7 +21967,7 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.6.5": +"core-js@npm:^3.26.0, core-js@npm:^3.6.5": version: 3.31.0 resolution: "core-js@npm:3.31.0" checksum: f7cf9b3010f7ca99c026d95b61743baca1a85512742ed2b67e8f65a72ac4f4fe0b90b00057783e886bdd39d3a295f42f845d33e7cba3973ed263df978343ab79 @@ -24923,6 +24992,7 @@ __metadata: "@types/react": "*" "@types/react-dom": "*" "@types/zen-observable": ^0.8.0 + backstage-plugin-analytics-module-nr: ^0.0.0 cross-env: ^7.0.0 cypress: ^10.0.0 eslint-plugin-cypress: ^2.10.3 @@ -25554,6 +25624,20 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.4.4": + version: 0.4.8 + resolution: "fflate@npm:0.4.8" + checksum: 29d8cbe44d5e7f53e7f5a160ac7f9cc025480c7b3bfd85c5f898cbe20dfa2dad4732daa534982664bf30b35896a90af44ea33ede5d94c5ffd1b8b0c0a0a56ca2 + languageName: node + linkType: hard + +"fflate@npm:^0.7.4": + version: 0.7.4 + resolution: "fflate@npm:0.7.4" + checksum: b812ab26047432db70ff4c73eb45ad53bd0774575b4818b9c61c2921e89ec65d1259f06ec1618f2ac55e6a2f2e29b6dc09173d213b46580bc69efae5344bf8f1 + languageName: node + linkType: hard + "figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -32437,6 +32521,13 @@ __metadata: languageName: node linkType: hard +"mitt@npm:^3.0.0": + version: 3.0.0 + resolution: "mitt@npm:3.0.0" + checksum: f7be5049d27d18b1dbe9408452d66376fa60ae4a79fe9319869d1b90ae8cbaedadc7e9dab30b32d781411256d468be5538996bb7368941c09009ef6bbfa6bfc7 + languageName: node + linkType: hard + "mixme@npm:^0.5.1": version: 0.5.4 resolution: "mixme@npm:0.5.4" @@ -37756,6 +37847,38 @@ __metadata: languageName: unknown linkType: soft +"rrdom@npm:^2.0.0-alpha.9": + version: 2.0.0-alpha.9 + resolution: "rrdom@npm:2.0.0-alpha.9" + dependencies: + rrweb-snapshot: ^2.0.0-alpha.9 + checksum: d56df9acc0348f4226a2d195692a422a431498c889a40046242f5643437d3388840de90078fb26488378fc250049a9d25ee5394a089f435e2f88668276bf17d4 + languageName: node + linkType: hard + +"rrweb-snapshot@npm:^2.0.0-alpha.9": + version: 2.0.0-alpha.9 + resolution: "rrweb-snapshot@npm:2.0.0-alpha.9" + checksum: 987f3ce493178dcc6782e959adef3e3f39e711bc8d5ac7dba5dfaf2fd01477aa2d3cd959598fa4e54cf15f2c397e95cf8323180a420857ca2a4d76abaec0a552 + languageName: node + linkType: hard + +"rrweb@npm:^2.0.0-alpha.8": + version: 2.0.0-alpha.9 + resolution: "rrweb@npm:2.0.0-alpha.9" + dependencies: + "@rrweb/types": ^2.0.0-alpha.9 + "@types/css-font-loading-module": 0.0.7 + "@xstate/fsm": ^1.4.0 + base64-arraybuffer: ^1.0.1 + fflate: ^0.4.4 + mitt: ^3.0.0 + rrdom: ^2.0.0-alpha.9 + rrweb-snapshot: ^2.0.0-alpha.9 + checksum: b87ec509dff99eef90496bc5685c57f6ec7868fb12c6b49a8d824a41e1a812aaa6b4aa792b50cdfdd68ec49287d51bba48ea764dfce149f3d9ce2363327328ff + languageName: node + linkType: hard + "rtl-css-js@npm:^1.14.0": version: 1.14.0 resolution: "rtl-css-js@npm:1.14.0" @@ -41673,6 +41796,13 @@ __metadata: languageName: node linkType: hard +"web-vitals@npm:^3.1.0": + version: 3.3.2 + resolution: "web-vitals@npm:3.3.2" + checksum: 76e832341d213d5de6f6767fef7f8c02163ab94326912a6355bbf6e194d930196ca5094b0d0d493b4c194acaaabcc96f0adc87870b913a7b9a51b225807abccf + languageName: node + linkType: hard + "webcrypto-core@npm:^1.7.4": version: 1.7.5 resolution: "webcrypto-core@npm:1.7.5" From 8ee279896ecace33df71d939033e0766e5dc9c02 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 14:33:24 +0200 Subject: [PATCH 014/329] Remove deprecated stuff Signed-off-by: Jonathan Mezach --- plugins/analytics-module-nr/dev/index.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/plugins/analytics-module-nr/dev/index.tsx b/plugins/analytics-module-nr/dev/index.tsx index 796bade4a9..bb57fb898f 100644 --- a/plugins/analytics-module-nr/dev/index.tsx +++ b/plugins/analytics-module-nr/dev/index.tsx @@ -24,17 +24,7 @@ import { import { Playground } from '../../analytics-module-nr/dev/Playground'; import { NewRelicBrowser } from '../src'; -/** - * @deprecated Importing and including this plugin in an app has no effect. - * This will be removed in a future release. - * - * @public - */ -export const analyticsModuleNR = createPlugin({ - id: 'analytics-provider-nr', -}); createDevApp() - .registerPlugin(analyticsModuleNR) .registerApi({ api: analyticsApiRef, deps: { configApi: configApiRef, identityApi: identityApiRef }, From 4decf61d8ba4a6db047a8c0a08527756a3393a58 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 14:47:08 +0200 Subject: [PATCH 015/329] Add some docs Signed-off-by: Jonathan Mezach --- plugins/analytics-module-nr/README.md | 100 ++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/plugins/analytics-module-nr/README.md b/plugins/analytics-module-nr/README.md index 131a7d9c63..ada06c991d 100644 --- a/plugins/analytics-module-nr/README.md +++ b/plugins/analytics-module-nr/README.md @@ -4,10 +4,100 @@ This plugin provides an opinionated implementation of the Backstage Analytics AP This plugin contains no other functionality. -## Getting started +## Installation -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/analytics-module-nr](http://localhost:3000/analytics-module-nr). +1. Install the plugin package in your Backstage app: -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. +```sh +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-analytics-module-nr +``` + +2. Wire up the API implementation to your App: + +```tsx +// packages/app/src/apis.ts +import { + analyticsApiRef, + configApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { NewRelicBrowser } from '@backstage/plugin-analytics-module-nr'; + +export const apis: AnyApiFactory[] = [ + // Instantiate and register the New Relic Browser API Implementation. + createApiFactory({ + api: analyticsApiRef, + deps: { configApi: configApiRef, identityApi: identityApiRef }, + factory: ({ configApi, identityApi }) => + NewRelicBrowser.fromConfig(configApi, { + identityApi, + }), + }), +]; +``` + +3. Configure the plugin in your `app-config.yaml`: + +The following is the minimum configuration required to start sending analytics +events to New Relic Browser. You find this information when creating a new application +in New Relic Browser using the Copy/Paste method. + +```yaml +# app-config.yaml +app: + analytics: + nr: + accountId: '1234567' + applicationId: '987654321' + licenseKey: 'NRJS-12a3456bc78de9123f4' + useEuEndpoint: false # Set this to true if you're using New Relic's EU data center +``` + +## Configuration + +By default the distributed tracing and cookies features are disabled. You can enable them by adding the following to your `app-config.yaml`: + +```yaml +# app-config.yaml +app: + analytics: + nr: + ... + distributedTracing: true + cookiesEnabled: true +``` + +## Development + +If you would like to contribute improvements to this plugin, the easiest way to +make and test changes is to do the following: + +1. Clone the main Backstage monorepo `git clone git@github.com:backstage/backstage.git` +2. Install all dependencies `yarn install` +3. If one does not exist, create an `app-config.local.yaml` file in the root of + the monorepo and add config for this plugin (see below) +4. Enter this plugin's working directory: `cd plugins/analytics-provider-nr` +5. Start the plugin in isolation: `yarn start` +6. Navigate to the playground page at `http://localhost:3000/nr` +7. Open the web console to see events fire when you navigate or when you + interact with instrumented components. + +Code for the isolated version of the plugin can be found inside the [/dev](./dev) +directory. Changes to the plugin are hot-reloaded. + +#### Recommended Dev Config + +Paste this into your `app-config.local.yaml` while developing this plugin: + +```yaml +app: + analytics: + nr: + accountId: '1234567' + applicationId: '987654321' + licenseKey: 'NRJS-12a3456bc78de9123f4' + distributedTracingEnabled: true + cookiesEnabled: true + useEuEndpoint: false +``` From ec73572588530e02d7b0ee945148ddd8f079d8a5 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 14:52:09 +0200 Subject: [PATCH 016/329] Add changeset Signed-off-by: Jonathan Mezach --- .changeset/rude-feet-sparkle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rude-feet-sparkle.md diff --git a/.changeset/rude-feet-sparkle.md b/.changeset/rude-feet-sparkle.md new file mode 100644 index 0000000000..09007bd663 --- /dev/null +++ b/.changeset/rude-feet-sparkle.md @@ -0,0 +1,5 @@ +--- +'backstage-plugin-analytics-module-nr': patch +--- + +Initial release From 8a398634d6688a193a6071f8d424b48eeaf032c8 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 15:12:34 +0200 Subject: [PATCH 017/329] Remove unused import Signed-off-by: Jonathan Mezach --- plugins/analytics-module-nr/dev/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/analytics-module-nr/dev/index.tsx b/plugins/analytics-module-nr/dev/index.tsx index bb57fb898f..ba492ae7ba 100644 --- a/plugins/analytics-module-nr/dev/index.tsx +++ b/plugins/analytics-module-nr/dev/index.tsx @@ -18,7 +18,6 @@ import { createDevApp } from '@backstage/dev-utils'; import { analyticsApiRef, configApiRef, - createPlugin, identityApiRef, } from '@backstage/core-plugin-api'; import { Playground } from '../../analytics-module-nr/dev/Playground'; From 122a48610b0a3ef6e23e63f2cdb4ccd9e44c4aec Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 15:18:39 +0200 Subject: [PATCH 018/329] Undo unnecessary change to example app Signed-off-by: Jonathan Mezach --- packages/app/package.json | 1 - yarn.lock | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 10482ffeb4..0f3a65c26a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -85,7 +85,6 @@ "@roadiehq/backstage-plugin-github-insights": "^2.0.5", "@roadiehq/backstage-plugin-github-pull-requests": "^2.2.7", "@roadiehq/backstage-plugin-travis-ci": "^2.0.5", - "backstage-plugin-analytics-module-nr": "^0.0.0", "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^17.0.2", diff --git a/yarn.lock b/yarn.lock index 96b1aefaea..1655772dbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19928,7 +19928,7 @@ __metadata: languageName: node linkType: hard -"backstage-plugin-analytics-module-nr@^0.0.0, backstage-plugin-analytics-module-nr@workspace:plugins/analytics-module-nr": +"backstage-plugin-analytics-module-nr@workspace:plugins/analytics-module-nr": version: 0.0.0-use.local resolution: "backstage-plugin-analytics-module-nr@workspace:plugins/analytics-module-nr" dependencies: @@ -24992,7 +24992,6 @@ __metadata: "@types/react": "*" "@types/react-dom": "*" "@types/zen-observable": ^0.8.0 - backstage-plugin-analytics-module-nr: ^0.0.0 cross-env: ^7.0.0 cypress: ^10.0.0 eslint-plugin-cypress: ^2.10.3 From 03bdc4380a1f484914b588d0e432f200b17336ed Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 15:45:08 +0200 Subject: [PATCH 019/329] Fix api report Signed-off-by: Jonathan Mezach --- plugins/analytics-module-nr/api-report.md | 25 +++++++++++++++++++ .../AnalyticsApi/NewRelicBrowser.ts | 7 ++++-- 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 plugins/analytics-module-nr/api-report.md diff --git a/plugins/analytics-module-nr/api-report.md b/plugins/analytics-module-nr/api-report.md new file mode 100644 index 0000000000..56c3d166d0 --- /dev/null +++ b/plugins/analytics-module-nr/api-report.md @@ -0,0 +1,25 @@ +## API Report File for "backstage-plugin-analytics-module-nr" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AnalyticsApi } from '@backstage/core-plugin-api'; +import { AnalyticsEvent } from '@backstage/core-plugin-api'; +import { Config } from '@backstage/config'; +import { IdentityApi } from '@backstage/core-plugin-api'; + +// @public +export class NewRelicBrowser implements AnalyticsApi { + // (undocumented) + captureEvent(event: AnalyticsEvent): void; + // (undocumented) + static fromConfig( + config: Config, + options: { + identityApi?: IdentityApi; + }, + ): NewRelicBrowser; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts index 5270dce156..35be8cc8a0 100644 --- a/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts +++ b/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts @@ -24,7 +24,7 @@ import type { setAPI } from '@newrelic/browser-agent/loaders/api/api'; type NewRelicAPI = ReturnType; -export type NewRelicBrowserOptions = { +type NewRelicBrowserOptions = { accountId: string; applicationId: string; licenseKey: string; @@ -33,7 +33,10 @@ export type NewRelicBrowserOptions = { useEuEndpoint: boolean; }; -// Implementation that optionally initializes with a userId. +/** + * New Relic Browser API provider for the Backstage Analytics API. + * @public + */ export class NewRelicBrowser implements AnalyticsApi { private readonly agent: NewRelicAPI; From 65596b0cf4a01cb7e6d15d75f809276f53e0d1ec Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 16:40:43 +0200 Subject: [PATCH 020/329] Respond to comments Signed-off-by: Jonathan Mezach --- .changeset/rude-feet-sparkle.md | 4 +- .github/CODEOWNERS | 2 +- plugins/analytics-module-nr/package.json | 6 +-- yarn.lock | 50 +++++++++++------------- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/.changeset/rude-feet-sparkle.md b/.changeset/rude-feet-sparkle.md index 09007bd663..96397af1e2 100644 --- a/.changeset/rude-feet-sparkle.md +++ b/.changeset/rude-feet-sparkle.md @@ -1,5 +1,5 @@ --- -'backstage-plugin-analytics-module-nr': patch +'@backstage/plugin-analytics-module-newrelic-browser': patch --- -Initial release +Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-nr) for more details! diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ea5b656bf7..a2d5f4988a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,7 +17,7 @@ /plugins/adr @backstage/maintainers @kuangp /plugins/adr-* @backstage/maintainers @kuangp /plugins/allure @backstage/maintainers @deepak-bhardwaj-ps -/plugins/analytics-module-nr @jmezach +/plugins/analytics-module-nr @backstage/maintainers @jmezach /plugins/apache-airflow @backstage/maintainers @cmpadden /plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers /plugins/azure-devops @backstage/maintainers @marleypowell @awanlin diff --git a/plugins/analytics-module-nr/package.json b/plugins/analytics-module-nr/package.json index 3cc446150a..6438855070 100644 --- a/plugins/analytics-module-nr/package.json +++ b/plugins/analytics-module-nr/package.json @@ -1,5 +1,5 @@ { - "name": "backstage-plugin-analytics-module-nr", + "name": "@backstage/plugin-analytics-module-newrelic-browser", "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", @@ -26,10 +26,6 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", - "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.13", - "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "^4.0.0-alpha.60", "@newrelic/browser-agent": "^1.236.0", "react-use": "^17.2.4" }, diff --git a/yarn.lock b/yarn.lock index 1655772dbc..f9fd557d64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4794,6 +4794,29 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-nr": + version: 0.0.0-use.local + resolution: "@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-nr" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@newrelic/browser-agent": ^1.236.0 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + msw: ^1.0.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-apache-airflow@workspace:^, @backstage/plugin-apache-airflow@workspace:plugins/apache-airflow": version: 0.0.0-use.local resolution: "@backstage/plugin-apache-airflow@workspace:plugins/apache-airflow" @@ -19928,33 +19951,6 @@ __metadata: languageName: node linkType: hard -"backstage-plugin-analytics-module-nr@workspace:plugins/analytics-module-nr": - version: 0.0.0-use.local - resolution: "backstage-plugin-analytics-module-nr@workspace:plugins/analytics-module-nr" - dependencies: - "@backstage/cli": "workspace:^" - "@backstage/config": "workspace:^" - "@backstage/core-app-api": "workspace:^" - "@backstage/core-components": "workspace:^" - "@backstage/core-plugin-api": "workspace:^" - "@backstage/dev-utils": "workspace:^" - "@backstage/test-utils": "workspace:^" - "@backstage/theme": "workspace:^" - "@material-ui/core": ^4.9.13 - "@material-ui/icons": ^4.9.1 - "@material-ui/lab": ^4.0.0-alpha.60 - "@newrelic/browser-agent": ^1.236.0 - "@testing-library/jest-dom": ^5.10.1 - "@testing-library/react": ^12.1.3 - "@testing-library/user-event": ^14.0.0 - "@types/node": "*" - msw: ^1.0.0 - react-use: ^17.2.4 - peerDependencies: - react: ^16.13.1 || ^17.0.0 - languageName: unknown - linkType: soft - "badge-maker@npm:^3.3.0": version: 3.3.1 resolution: "badge-maker@npm:3.3.1" From cb72eb6370d850451ae2a3821953741b2e43b5e8 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 4 Jul 2023 18:29:28 +0200 Subject: [PATCH 021/329] Fix API report Signed-off-by: Jonathan Mezach --- plugins/analytics-module-nr/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/analytics-module-nr/api-report.md b/plugins/analytics-module-nr/api-report.md index 56c3d166d0..26b478f64d 100644 --- a/plugins/analytics-module-nr/api-report.md +++ b/plugins/analytics-module-nr/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "backstage-plugin-analytics-module-nr" +## API Report File for "@backstage/plugin-analytics-module-newrelic-browser" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). From 4100ef10014cbe06bcf14c3cd153579237ebd4ff Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 5 Jul 2023 11:02:21 +0200 Subject: [PATCH 022/329] Renamed folder to reflect new name Signed-off-by: Jonathan Mezach --- .../.eslintrc.js | 0 .../README.md | 0 .../api-report.md | 0 .../config.d.ts | 0 .../dev/Playground.tsx | 0 .../dev/index.tsx | 2 +- .../package.json | 0 .../src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts | 0 .../src/apis/implementations/AnalyticsApi/index.ts | 0 .../src/index.ts | 0 .../src/setupTests.ts | 0 yarn.lock | 4 ++-- 12 files changed, 3 insertions(+), 3 deletions(-) rename plugins/{analytics-module-nr => analytics-module-new-relic}/.eslintrc.js (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/README.md (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/api-report.md (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/config.d.ts (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/dev/Playground.tsx (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/dev/index.tsx (94%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/package.json (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/src/apis/implementations/AnalyticsApi/index.ts (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/src/index.ts (100%) rename plugins/{analytics-module-nr => analytics-module-new-relic}/src/setupTests.ts (100%) diff --git a/plugins/analytics-module-nr/.eslintrc.js b/plugins/analytics-module-new-relic/.eslintrc.js similarity index 100% rename from plugins/analytics-module-nr/.eslintrc.js rename to plugins/analytics-module-new-relic/.eslintrc.js diff --git a/plugins/analytics-module-nr/README.md b/plugins/analytics-module-new-relic/README.md similarity index 100% rename from plugins/analytics-module-nr/README.md rename to plugins/analytics-module-new-relic/README.md diff --git a/plugins/analytics-module-nr/api-report.md b/plugins/analytics-module-new-relic/api-report.md similarity index 100% rename from plugins/analytics-module-nr/api-report.md rename to plugins/analytics-module-new-relic/api-report.md diff --git a/plugins/analytics-module-nr/config.d.ts b/plugins/analytics-module-new-relic/config.d.ts similarity index 100% rename from plugins/analytics-module-nr/config.d.ts rename to plugins/analytics-module-new-relic/config.d.ts diff --git a/plugins/analytics-module-nr/dev/Playground.tsx b/plugins/analytics-module-new-relic/dev/Playground.tsx similarity index 100% rename from plugins/analytics-module-nr/dev/Playground.tsx rename to plugins/analytics-module-new-relic/dev/Playground.tsx diff --git a/plugins/analytics-module-nr/dev/index.tsx b/plugins/analytics-module-new-relic/dev/index.tsx similarity index 94% rename from plugins/analytics-module-nr/dev/index.tsx rename to plugins/analytics-module-new-relic/dev/index.tsx index ba492ae7ba..5524a48b68 100644 --- a/plugins/analytics-module-nr/dev/index.tsx +++ b/plugins/analytics-module-new-relic/dev/index.tsx @@ -20,7 +20,7 @@ import { configApiRef, identityApiRef, } from '@backstage/core-plugin-api'; -import { Playground } from '../../analytics-module-nr/dev/Playground'; +import { Playground } from './Playground'; import { NewRelicBrowser } from '../src'; createDevApp() diff --git a/plugins/analytics-module-nr/package.json b/plugins/analytics-module-new-relic/package.json similarity index 100% rename from plugins/analytics-module-nr/package.json rename to plugins/analytics-module-new-relic/package.json diff --git a/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts similarity index 100% rename from plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts rename to plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts diff --git a/plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/index.ts b/plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/index.ts similarity index 100% rename from plugins/analytics-module-nr/src/apis/implementations/AnalyticsApi/index.ts rename to plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/index.ts diff --git a/plugins/analytics-module-nr/src/index.ts b/plugins/analytics-module-new-relic/src/index.ts similarity index 100% rename from plugins/analytics-module-nr/src/index.ts rename to plugins/analytics-module-new-relic/src/index.ts diff --git a/plugins/analytics-module-nr/src/setupTests.ts b/plugins/analytics-module-new-relic/src/setupTests.ts similarity index 100% rename from plugins/analytics-module-nr/src/setupTests.ts rename to plugins/analytics-module-new-relic/src/setupTests.ts diff --git a/yarn.lock b/yarn.lock index f9fd557d64..89a006567c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4794,9 +4794,9 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-nr": +"@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-new-relic": version: 0.0.0-use.local - resolution: "@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-nr" + resolution: "@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-new-relic" dependencies: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" From 2f4872d44cafa39a85da2281c8133a05b1f47443 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 5 Jul 2023 11:25:10 +0200 Subject: [PATCH 023/329] Fix broken link Signed-off-by: Jonathan Mezach --- .changeset/rude-feet-sparkle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rude-feet-sparkle.md b/.changeset/rude-feet-sparkle.md index 96397af1e2..12cb5fd1f6 100644 --- a/.changeset/rude-feet-sparkle.md +++ b/.changeset/rude-feet-sparkle.md @@ -2,4 +2,4 @@ '@backstage/plugin-analytics-module-newrelic-browser': patch --- -Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-nr) for more details! +Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-new-relic) for more details! From 7cb22175993ab4df2c8beebbd84b96760d3e6ca5 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Wed, 5 Jul 2023 13:02:51 -0400 Subject: [PATCH 024/329] improve serviceAccountToken docs Give instructions for k8s 1.24+, and mention the in-cluster option Signed-off-by: Jamie Klassen --- docs/features/kubernetes/configuration.md | 35 +++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 9280c4f020..5b2d72069f 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -110,7 +110,7 @@ cluster. Valid values are: | `google` | This will use a user's Google access token from the [Google auth provider](https://backstage.io/docs/auth/google/provider) to access the Kubernetes API on GKE clusters. | | `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters | | `oidc` | This will use [Oidc Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) to authenticate to the Kubernetes API. When this is used the `oidcTokenProvider` field should also be set. Please note the cluster must support OIDC, at the time of writing AKS clusters do not support OIDC. | -| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. | +| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set, or else Backstage should be running in-cluster. | Check the [Kubernetes Authentication][4] section for additional explanation. @@ -127,7 +127,9 @@ CPU/Memory for pods returned by the API server. Defaults to `false`. ##### `clusters.\*.serviceAccountToken` (optional) The service account token to be used when using the `serviceAccount` auth -provider. You could get the service account token with: +provider. On versions of Kubernetes [prior to +1.24](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.24.md#no-really-you-must-read-this-before-you-upgrade-1), +you could get an (automatically-generated) token for a service account with: ```sh kubectl -n get secret $(kubectl -n get sa -o=json \ @@ -136,6 +138,35 @@ kubectl -n get secret $(kubectl -n get sa + namespace: + annotations: + kubernetes.io/service-account.name: +type: kubernetes.io/service-account-token +EOF +``` + +waiting for the token controller to populate a token, and retrieving it with: + +```sh +kubectl -n get secret -o go-template='{{.data.token | base64decode}}' +``` + +If a cluster has `authProvider: serviceAccount` and the `serviceAccountToken` +field is omitted, Backstage will ignore the configured URL and certificate data, +instead attempting to access the Kubernetes API via an in-cluster client as in +[this +example](https://github.com/kubernetes-client/javascript/blob/master/examples/in-cluster.js). + ##### `clusters.\*.oidcTokenProvider` (optional) This field is to be used when using the `oidc` auth provider. It will use the id tokens From 73a5205447dccd188558549145dc180562e2c52d Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Wed, 5 Jul 2023 18:47:52 +0100 Subject: [PATCH 025/329] fix: Use real entity instead of ref for attributes Signed-off-by: Mike Bryant --- .../DefaultCatalogProcessingEngine.ts | 2 +- .../DefaultCatalogProcessingOrchestrator.ts | 18 ++++++------ .../catalog-backend/src/util/opentelemetry.ts | 29 ++++++++++++------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index 2ae104666f..bed1fe6f50 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -140,7 +140,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { processTask: async item => { await withActiveSpan(tracer, 'ProcessingRun', async span => { const track = this.tracker.processStart(item, this.logger); - addEntityAttributes(span, item.entityRef); + addEntityAttributes(span, item.unprocessedEntity); try { const { diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 98f9cd32a5..c5eaacbc68 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -200,7 +200,7 @@ export class DefaultCatalogProcessingOrchestrator context: Context, ): Promise { return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { - addEntityAttributes(stageSpan, context.entityRef); + addEntityAttributes(stageSpan, entity); stageSpan.setAttribute('backstage.catalog.processor.stage', 'preProcess'); let res = entity; @@ -208,7 +208,7 @@ export class DefaultCatalogProcessingOrchestrator if (processor.preProcessEntity) { let innerRes = res; res = await withActiveSpan(tracer, 'ProcessingStep', async span => { - addEntityAttributes(span, context.entityRef); + addEntityAttributes(span, entity); addProcessorAttributes(span, 'preProcessEntity', processor); try { innerRes = await processor.preProcessEntity!( @@ -238,7 +238,7 @@ export class DefaultCatalogProcessingOrchestrator */ private async runPolicyStep(entity: Entity): Promise { return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { - addEntityAttributes(stageSpan, stringifyEntityRef(entity)); + addEntityAttributes(stageSpan, entity); stageSpan.setAttribute( 'backstage.catalog.processor.stage', 'enforcePolicy', @@ -274,7 +274,7 @@ export class DefaultCatalogProcessingOrchestrator context: Context, ): Promise { return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { - addEntityAttributes(stageSpan, context.entityRef); + addEntityAttributes(stageSpan, entity); stageSpan.setAttribute('backstage.catalog.processor.stage', 'validate'); // Double check that none of the previous steps tried to change something // related to the entity ref, which would break downstream @@ -303,7 +303,7 @@ export class DefaultCatalogProcessingOrchestrator tracer, 'ProcessingStep', async span => { - addEntityAttributes(span, context.entityRef); + addEntityAttributes(span, entity); addProcessorAttributes(span, 'validateEntityKind', processor); return await processor.validateEntityKind!(entity); }, @@ -339,7 +339,7 @@ export class DefaultCatalogProcessingOrchestrator context: Context, ): Promise { return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { - addEntityAttributes(stageSpan, context.entityRef); + addEntityAttributes(stageSpan, entity); stageSpan.setAttribute( 'backstage.catalog.processor.stage', 'readLocation', @@ -379,7 +379,7 @@ export class DefaultCatalogProcessingOrchestrator tracer, 'ProcessingStep', async span => { - addEntityAttributes(span, context.entityRef); + addEntityAttributes(span, entity); addProcessorAttributes(span, 'readLocation', processor); return await processor.readLocation!( { @@ -423,7 +423,7 @@ export class DefaultCatalogProcessingOrchestrator context: Context, ): Promise { return await withActiveSpan(tracer, 'ProcessingStage', async stageSpan => { - addEntityAttributes(stageSpan, context.entityRef); + addEntityAttributes(stageSpan, entity); stageSpan.setAttribute( 'backstage.catalog.processor.stage', 'postProcessEntity', @@ -434,7 +434,7 @@ export class DefaultCatalogProcessingOrchestrator if (processor.postProcessEntity) { let innerRes = res; res = await withActiveSpan(tracer, 'ProcessingStep', async span => { - addEntityAttributes(span, context.entityRef); + addEntityAttributes(span, entity); addProcessorAttributes(span, 'postProcessEntity', processor); try { innerRes = await processor.postProcessEntity!( diff --git a/plugins/catalog-backend/src/util/opentelemetry.ts b/plugins/catalog-backend/src/util/opentelemetry.ts index 391869deb8..c2df008452 100644 --- a/plugins/catalog-backend/src/util/opentelemetry.ts +++ b/plugins/catalog-backend/src/util/opentelemetry.ts @@ -15,22 +15,31 @@ */ import { Span, SpanOptions, SpanStatusCode, Tracer } from '@opentelemetry/api'; -import { parseEntityRef } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; export const TRACER_ID = 'backstage-plugin-catalog-backend'; -export function addEntityAttributes(span: Span, entityRef: string) { - try { - const fields = parseEntityRef(entityRef); - span.setAttribute('backstage.entity.kind', fields.kind); - span.setAttribute('backstage.entity.namespace', fields.namespace); - span.setAttribute('backstage.entity.name', fields.name); - } catch (err) { - span.recordException(err); - span.setStatus({ code: SpanStatusCode.ERROR }); +function setAttributeIfDefined(span: Span, attribute: string, value?: string) { + if (value !== null && value !== undefined) { + span.setAttribute(attribute, value); } } +export function addEntityAttributes(span: Span, entity: Entity) { + setAttributeIfDefined(span, 'backstage.entity.apiVersion', entity.apiVersion); + setAttributeIfDefined(span, 'backstage.entity.kind', entity.kind); + setAttributeIfDefined( + span, + 'backstage.entity.metadata.namespace', + entity.metadata?.namespace, + ); + setAttributeIfDefined( + span, + 'backstage.entity.metadata.name', + entity.metadata?.name, + ); +} + // Adapted from https://github.com/open-telemetry/opentelemetry-js/blob/359fbcc40a859057a02b14e84599eac399b8dba7/api/src/trace/SugaredTracer.ts // While waiting for something like https://github.com/open-telemetry/opentelemetry-js/pull/3317 to land upstream From 14c6729ec4cff70a344c5423c32e88684476ab87 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Fri, 7 Jul 2023 08:55:43 +0200 Subject: [PATCH 026/329] Changed package role Signed-off-by: Jonathan Mezach --- plugins/analytics-module-new-relic/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/analytics-module-new-relic/package.json b/plugins/analytics-module-new-relic/package.json index 6438855070..9364899003 100644 --- a/plugins/analytics-module-new-relic/package.json +++ b/plugins/analytics-module-new-relic/package.json @@ -11,7 +11,7 @@ "types": "dist/index.d.ts" }, "backstage": { - "role": "frontend-plugin" + "role": "frontend-plugin-module" }, "scripts": { "start": "backstage-cli package start", From bbd52b2669bcedb4e929a03b05721b4865b6ecf2 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Fri, 7 Jul 2023 12:20:55 +0200 Subject: [PATCH 027/329] Add section about User IDs to README Signed-off-by: Jonathan Mezach --- plugins/analytics-module-new-relic/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/analytics-module-new-relic/README.md b/plugins/analytics-module-new-relic/README.md index ada06c991d..d278597c93 100644 --- a/plugins/analytics-module-new-relic/README.md +++ b/plugins/analytics-module-new-relic/README.md @@ -68,6 +68,10 @@ app: cookiesEnabled: true ``` +### User IDs + +This plugin supports sending user context to New Relic Browser by providing a User ID. This requires instantiating the `NewRelicBrowser` instance with an `identityApi` instance passed to it, but this is optional. If omitted the plugin will not send user context to New Relic Browser. + ## Development If you would like to contribute improvements to this plugin, the easiest way to From 4e2ec6a308d91967119e054448e78017ff88b304 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 02:47:48 +0200 Subject: [PATCH 028/329] Update WelcomeTitle.tsx Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../WelcomeTitle/WelcomeTitle.tsx | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx index 49ae38d90c..d4c39e2a29 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx @@ -13,39 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - alertApiRef, - identityApiRef, - useApi, -} from '@backstage/core-plugin-api'; -import { Tooltip, Typography } from '@material-ui/core'; -import React, { useEffect, useMemo } from 'react'; -import useAsync from 'react-use/lib/useAsync'; -import { getTimeBasedGreeting } from './timeUtil'; +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { WelcomeTitle } from './WelcomeTitle'; -export const WelcomeTitle = () => { - const identityApi = useApi(identityApiRef); - const alertApi = useApi(alertApiRef); - const greeting = useMemo(() => getTimeBasedGreeting(), []); +describe('', () => { + afterEach(() => jest.resetAllMocks()); - const { value: profile, error } = useAsync(() => - identityApi.getProfileInfo(), - ); + test('should greet user with default greeting', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('1970-01-01T23:00:00').valueOf()); - useEffect(() => { - if (error) { - alertApi.post({ - message: `Failed to load user identity: ${error}`, - severity: 'error', - }); - } - }, [error, alertApi]); + const { getByText } = await renderInTestApp(); - return ( - - {`${greeting.greeting}${ - profile?.displayName ? `, ${profile?.displayName}` : '' - }!`} - - ); -}; + expect(getByText(/Get some rest, Guest/)).toBeInTheDocument(); + }); + + test('should greet user with multiple languages', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2023-07-03T14:00:00').valueOf()); + + const languages = ['English', 'Spanish']; + const { getByText } = await renderInTestApp( + , + ); + + expect(getByText(/Good afternoon, Guest/)).toBeInTheDocument(); + }); +}); From e547a8a3b8d8fc085ece3b52cb2173cb5c314237 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 02:48:13 +0200 Subject: [PATCH 029/329] Update WelcomeTitle.tsx Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../WelcomeTitle/WelcomeTitle.tsx | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx index d4c39e2a29..d32ff42ea5 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx @@ -13,33 +13,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; -import { WelcomeTitle } from './WelcomeTitle'; +import { + alertApiRef, + identityApiRef, + useApi, +} from '@backstage/core-plugin-api'; +import { Tooltip, Typography } from '@material-ui/core'; +import React, { useEffect, useMemo } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { getTimeBasedGreeting } from './timeUtil'; -describe('', () => { - afterEach(() => jest.resetAllMocks()); +interface WelcomeTitleLanguageProps { + language?: string[]; +} - test('should greet user with default greeting', async () => { - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('1970-01-01T23:00:00').valueOf()); +export const WelcomeTitle = ({ language }: WelcomeTitleLanguageProps) => { + const identityApi = useApi(identityApiRef); + const alertApi = useApi(alertApiRef); + const greeting = useMemo(() => getTimeBasedGreeting(language), [language]); - const { getByText } = await renderInTestApp(); + const { value: profile, error } = useAsync(() => + identityApi.getProfileInfo(), + ); - expect(getByText(/Get some rest, Guest/)).toBeInTheDocument(); - }); + useEffect(() => { + if (error) { + alertApi.post({ + message: `Failed to load user identity: ${error}`, + severity: 'error', + }); + } + }, [error, alertApi]); - test('should greet user with multiple languages', async () => { - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('2023-07-03T14:00:00').valueOf()); - - const languages = ['English', 'Spanish']; - const { getByText } = await renderInTestApp( - , - ); - - expect(getByText(/Good afternoon, Guest/)).toBeInTheDocument(); - }); -}); + return ( + + {`${greeting.greeting}${ + profile?.displayName ? `, ${profile?.displayName}` : '' + }!`} + + ); +}; From 0ec9d0c9192839cd3dd4cdbc2a91b32eaa40fcdc Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 02:48:33 +0200 Subject: [PATCH 030/329] Update timeUtil.ts Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../WelcomeTitle/timeUtil.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/timeUtil.ts b/plugins/home/src/homePageComponents/WelcomeTitle/timeUtil.ts index 8df7703b44..ee8f8fe556 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/timeUtil.ts +++ b/plugins/home/src/homePageComponents/WelcomeTitle/timeUtil.ts @@ -22,7 +22,10 @@ import goodEvening from './locales/goodEvening.locales.json'; // every time the user navigates. const greetingRandomSeed = Math.floor(Math.random() * 1000000); -export function getTimeBasedGreeting(): { language: string; greeting: string } { +export function getTimeBasedGreeting(language?: string[] | undefined): { + language: string; + greeting: string; +} { const random = (array: string[]) => array[greetingRandomSeed % array.length]; const currentHour = new Date(Date.now()).getHours(); @@ -32,12 +35,26 @@ export function getTimeBasedGreeting(): { language: string; greeting: string } { greeting: 'Get some rest', }; } + const timeOfDay = (hour: number): { [language: string]: string } => { if (hour < 12) return goodMorning; if (hour < 17) return goodAfternoon; return goodEvening; }; + const greetings = timeOfDay(currentHour); + + if (Array.isArray(language) && language.length > 0) { + const validLanguages = language.filter(lang => lang && greetings[lang]); + if (validLanguages.length > 0) { + const greetingsKey = random(validLanguages); + return { + language: greetingsKey, + greeting: greetings[greetingsKey], + }; + } + } + const greetingsKey = random(Object.keys(greetings)); return { language: greetingsKey, From 7492035327752cc91db6aa4f5e37f66517e5e07c Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 02:48:56 +0200 Subject: [PATCH 031/329] Update WelcomeTitle.test.tsx Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../WelcomeTitle/WelcomeTitle.test.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx index 839ecdbae1..d4c39e2a29 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx @@ -20,7 +20,7 @@ import { WelcomeTitle } from './WelcomeTitle'; describe('', () => { afterEach(() => jest.resetAllMocks()); - test('should greet user', async () => { + test('should greet user with default greeting', async () => { jest .spyOn(global.Date, 'now') .mockImplementation(() => new Date('1970-01-01T23:00:00').valueOf()); @@ -29,4 +29,17 @@ describe('', () => { expect(getByText(/Get some rest, Guest/)).toBeInTheDocument(); }); + + test('should greet user with multiple languages', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('2023-07-03T14:00:00').valueOf()); + + const languages = ['English', 'Spanish']; + const { getByText } = await renderInTestApp( + , + ); + + expect(getByText(/Good afternoon, Guest/)).toBeInTheDocument(); + }); }); From dfd0b8d16b886b0e077c8eb658f53bab766537ba Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 02:49:14 +0200 Subject: [PATCH 032/329] Update WelcomeTitle.test.tsx Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../WelcomeTitle/WelcomeTitle.test.tsx | 50 +++++++++---------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx index d4c39e2a29..6d14aa7a95 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2023 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,33 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; + +import { Header } from '@backstage/core-components'; +import { wrapInTestApp } from '@backstage/test-utils'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { WelcomeTitle } from './WelcomeTitle'; -describe('', () => { - afterEach(() => jest.resetAllMocks()); +export default { + title: 'Plugins/Home/Components/WelcomeTitle', + decorators: [ + (Story: ComponentType>) => wrapInTestApp(), + ], +}; - test('should greet user with default greeting', async () => { - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('1970-01-01T23:00:00').valueOf()); +export const Default = () => { + return
} pageTitleOverride="Home" />; +}; - const { getByText } = await renderInTestApp(); - - expect(getByText(/Get some rest, Guest/)).toBeInTheDocument(); - }); - - test('should greet user with multiple languages', async () => { - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('2023-07-03T14:00:00').valueOf()); - - const languages = ['English', 'Spanish']; - const { getByText } = await renderInTestApp( - , - ); - - expect(getByText(/Good afternoon, Guest/)).toBeInTheDocument(); - }); -}); +export const withLanguage = () => { + const languages = ['English', 'Spanish']; + return ( +
} + pageTitleOverride="Home" + /> + ); +}; From a559ff68de7e4e59f5b29a80cfd206e7b0d38823 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 02:53:32 +0200 Subject: [PATCH 033/329] Create quiet-starfishes-kick.md Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .changeset/quiet-starfishes-kick.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-starfishes-kick.md diff --git a/.changeset/quiet-starfishes-kick.md b/.changeset/quiet-starfishes-kick.md new file mode 100644 index 0000000000..b8d7b8f7c2 --- /dev/null +++ b/.changeset/quiet-starfishes-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +The `getTimeBasedGreeting` function has been updated to support an optional language parameter. Now, callers of the function can provide a language as input to receive a greeting in that specific language. If no language is provided, the function will automatically select a random greeting based on the current time. From 87176d85875bf08b6d31c51ee7aa453a4caeda4d Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 11:22:12 +0200 Subject: [PATCH 034/329] Update index.ts Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- plugins/home/src/homePageComponents/WelcomeTitle/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/index.ts b/plugins/home/src/homePageComponents/WelcomeTitle/index.ts index ca237511e7..8ca4d4bcc0 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/index.ts +++ b/plugins/home/src/homePageComponents/WelcomeTitle/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { WelcomeTitle } from './WelcomeTitle'; +export type { WelcomeTitleLanguageProps } from './WelcomeTitle'; From a2c9987cc502e5fc8201efd32b40a6773b12ef9d Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 11:22:50 +0200 Subject: [PATCH 035/329] Update WelcomeTitle.tsx Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx index d32ff42ea5..84753a9fbb 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx @@ -23,9 +23,9 @@ import React, { useEffect, useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { getTimeBasedGreeting } from './timeUtil'; -interface WelcomeTitleLanguageProps { +export type WelcomeTitleLanguageProps = { language?: string[]; -} +}; export const WelcomeTitle = ({ language }: WelcomeTitleLanguageProps) => { const identityApi = useApi(identityApiRef); From fdd1b471bcd65b1f7faf4500de632e403e3ca588 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 12:02:36 +0200 Subject: [PATCH 036/329] Update index.ts Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- plugins/home/src/homePageComponents/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/home/src/homePageComponents/index.ts b/plugins/home/src/homePageComponents/index.ts index 6a3978c595..36d2335afe 100644 --- a/plugins/home/src/homePageComponents/index.ts +++ b/plugins/home/src/homePageComponents/index.ts @@ -16,3 +16,4 @@ export type { ToolkitContentProps, Tool } from './Toolkit'; export type { ClockConfig } from './HeaderWorldClock'; +export type { WelcomeTitleLanguageProps } from './WelcomeTitle'; From ddc7ec4f8fe9e0123ef64f08970f27a8e7c526eb Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 12:33:34 +0200 Subject: [PATCH 037/329] Added Test case Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../WelcomeTitle/WelcomeTitle.test.tsx | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx index 6d14aa7a95..8a8b1bae0b 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx @@ -14,28 +14,32 @@ * limitations under the License. */ -import { Header } from '@backstage/core-components'; -import { wrapInTestApp } from '@backstage/test-utils'; -import React, { ComponentType, PropsWithChildren } from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; import { WelcomeTitle } from './WelcomeTitle'; -export default { - title: 'Plugins/Home/Components/WelcomeTitle', - decorators: [ - (Story: ComponentType>) => wrapInTestApp(), - ], -}; +describe('', () => { + afterEach(() => jest.resetAllMocks()); -export const Default = () => { - return
} pageTitleOverride="Home" />; -}; + test('should greet user', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('1970-01-01T23:00:00').valueOf()); -export const withLanguage = () => { - const languages = ['English', 'Spanish']; - return ( -
} - pageTitleOverride="Home" - /> + const { getByText } = await renderInTestApp(); + + expect(getByText(/Get some rest, Guest/)).toBeInTheDocument(); + }); +}); + +test('should greet user with a single language', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('1970-01-01T10:00:00').valueOf()); + + const { getByText } = await renderInTestApp( + , ); -}; + + expect(getByText(/Good morning, Guest/)).toBeInTheDocument(); +}); From b4c3065480c1a5d71a0638503c463307a0bd7009 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 12:34:14 +0200 Subject: [PATCH 038/329] Update Storybook Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../WelcomeTitle/WelcomeTitle.stories.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx index d7d12f02c6..6d14aa7a95 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.stories.tsx @@ -29,3 +29,13 @@ export default { export const Default = () => { return
} pageTitleOverride="Home" />; }; + +export const withLanguage = () => { + const languages = ['English', 'Spanish']; + return ( +
} + pageTitleOverride="Home" + /> + ); +}; From 54e3cfe03884fbe28a0c236c2e7e2a6179cb5a15 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 13:00:12 +0200 Subject: [PATCH 039/329] Update api-report.md Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- plugins/home/api-report.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 1001f27b51..34e81dce1b 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -44,7 +44,7 @@ export type ClockConfig = { export const ComponentAccordion: (props: { title: string; expanded?: boolean | undefined; - Content: () => JSX.Element; + Content: () => JSX.Element /** @public */; Actions?: (() => JSX.Element) | undefined; Settings?: (() => JSX.Element) | undefined; ContextProvider?: ((props: any) => JSX.Element) | undefined; @@ -184,5 +184,12 @@ export type ToolkitContentProps = { }; // @public -export const WelcomeTitle: () => JSX.Element; +export const WelcomeTitle: ({ + language, +}: WelcomeTitleLanguageProps) => JSX.Element; + +// @public (undocumented) +export type WelcomeTitleLanguageProps = { + language?: string[]; +}; ``` From 2c0746d2af4ecaaabd464c7377c858b8095ebd1e Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 13:00:38 +0200 Subject: [PATCH 040/329] Update WelcomeTitle.tsx Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx index 84753a9fbb..9981d91873 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx @@ -23,6 +23,7 @@ import React, { useEffect, useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { getTimeBasedGreeting } from './timeUtil'; +/** @public */ export type WelcomeTitleLanguageProps = { language?: string[]; }; From be8aa3a8f5f7fb0159d067d15e73860365402558 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 13:22:13 +0200 Subject: [PATCH 041/329] Update api-report.md Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> From d74425050524f7a1e5b0205e2a552f552fb25254 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 19:53:39 +0200 Subject: [PATCH 042/329] Fix API report Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- plugins/home/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 34e81dce1b..5442714430 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -44,7 +44,7 @@ export type ClockConfig = { export const ComponentAccordion: (props: { title: string; expanded?: boolean | undefined; - Content: () => JSX.Element /** @public */; + Content: () => JSX.Element; Actions?: (() => JSX.Element) | undefined; Settings?: (() => JSX.Element) | undefined; ContextProvider?: ((props: any) => JSX.Element) | undefined; From 7d2ef2b57297a23dda1b7b2abe0f81ad513d3f47 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 19:54:46 +0200 Subject: [PATCH 043/329] update copyright date Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .../src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx index 8a8b1bae0b..dfeac300c4 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2021 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 28ebdf9023b080fe8c10a050e2d8099e2f713342 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 19:57:06 +0200 Subject: [PATCH 044/329] Update quiet-starfishes-kick.md Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .changeset/quiet-starfishes-kick.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-starfishes-kick.md b/.changeset/quiet-starfishes-kick.md index b8d7b8f7c2..162a9e0467 100644 --- a/.changeset/quiet-starfishes-kick.md +++ b/.changeset/quiet-starfishes-kick.md @@ -2,4 +2,4 @@ '@backstage/plugin-home': patch --- -The `getTimeBasedGreeting` function has been updated to support an optional language parameter. Now, callers of the function can provide a language as input to receive a greeting in that specific language. If no language is provided, the function will automatically select a random greeting based on the current time. +Now, user can provide a language (optional) as input to receive a greeting in that specific language. Example: From bd3b824cce7b935befba9d7c1dc5c72bb1127cf4 Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 20:12:02 +0200 Subject: [PATCH 045/329] Update quiet-starfishes-kick.md Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .changeset/quiet-starfishes-kick.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-starfishes-kick.md b/.changeset/quiet-starfishes-kick.md index 162a9e0467..548e28aa31 100644 --- a/.changeset/quiet-starfishes-kick.md +++ b/.changeset/quiet-starfishes-kick.md @@ -2,4 +2,4 @@ '@backstage/plugin-home': patch --- -Now, user can provide a language (optional) as input to receive a greeting in that specific language. Example: +Now, user can provide a language (optional) as input to receive a greeting in that specific language. Example: `````` From 35c7cadf15256833938826551257d3b0a0c71c7b Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> Date: Sun, 9 Jul 2023 20:22:06 +0200 Subject: [PATCH 046/329] Ran prettier Signed-off-by: Gaurav Pandey <36168816+grvpandey11@users.noreply.github.com> --- .changeset/quiet-starfishes-kick.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-starfishes-kick.md b/.changeset/quiet-starfishes-kick.md index 548e28aa31..af9ee4b64e 100644 --- a/.changeset/quiet-starfishes-kick.md +++ b/.changeset/quiet-starfishes-kick.md @@ -2,4 +2,4 @@ '@backstage/plugin-home': patch --- -Now, user can provide a language (optional) as input to receive a greeting in that specific language. Example: `````` +Now, user can provide a language (optional) as input to receive a greeting in that specific language. Example: `` From 7352d05fde6ce44af4e9ad762d6361ad24daeee7 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Mon, 10 Jul 2023 13:33:55 +0200 Subject: [PATCH 047/329] Fix a docs issue Signed-off-by: Jonathan Mezach --- plugins/analytics-module-new-relic/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/analytics-module-new-relic/README.md b/plugins/analytics-module-new-relic/README.md index d278597c93..98f17eae20 100644 --- a/plugins/analytics-module-new-relic/README.md +++ b/plugins/analytics-module-new-relic/README.md @@ -22,7 +22,7 @@ import { configApiRef, identityApiRef, } from '@backstage/core-plugin-api'; -import { NewRelicBrowser } from '@backstage/plugin-analytics-module-nr'; +import { NewRelicBrowser } from '@backstage/plugin-analytics-module-newrelic-browser'; export const apis: AnyApiFactory[] = [ // Instantiate and register the New Relic Browser API Implementation. From bc438208993272319baef6ec079fcc225ee5f130 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 11:30:17 +0200 Subject: [PATCH 048/329] Rename again to match module name Signed-off-by: Jonathan Mezach --- .../.eslintrc.js | 0 .../README.md | 0 .../api-report.md | 0 .../config.d.ts | 0 .../dev/Playground.tsx | 0 .../dev/index.tsx | 0 .../package.json | 0 .../AnalyticsApi/NewRelicBrowser.ts | 33 +++++++++++++++++-- .../implementations/AnalyticsApi/index.ts | 0 .../src/index.ts | 0 .../src/setupTests.ts | 0 yarn.lock | 4 +-- 12 files changed, 33 insertions(+), 4 deletions(-) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/.eslintrc.js (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/README.md (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/api-report.md (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/config.d.ts (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/dev/Playground.tsx (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/dev/index.tsx (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/package.json (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts (76%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/src/apis/implementations/AnalyticsApi/index.ts (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/src/index.ts (100%) rename plugins/{analytics-module-new-relic => analytics-module-newrelic-browser}/src/setupTests.ts (100%) diff --git a/plugins/analytics-module-new-relic/.eslintrc.js b/plugins/analytics-module-newrelic-browser/.eslintrc.js similarity index 100% rename from plugins/analytics-module-new-relic/.eslintrc.js rename to plugins/analytics-module-newrelic-browser/.eslintrc.js diff --git a/plugins/analytics-module-new-relic/README.md b/plugins/analytics-module-newrelic-browser/README.md similarity index 100% rename from plugins/analytics-module-new-relic/README.md rename to plugins/analytics-module-newrelic-browser/README.md diff --git a/plugins/analytics-module-new-relic/api-report.md b/plugins/analytics-module-newrelic-browser/api-report.md similarity index 100% rename from plugins/analytics-module-new-relic/api-report.md rename to plugins/analytics-module-newrelic-browser/api-report.md diff --git a/plugins/analytics-module-new-relic/config.d.ts b/plugins/analytics-module-newrelic-browser/config.d.ts similarity index 100% rename from plugins/analytics-module-new-relic/config.d.ts rename to plugins/analytics-module-newrelic-browser/config.d.ts diff --git a/plugins/analytics-module-new-relic/dev/Playground.tsx b/plugins/analytics-module-newrelic-browser/dev/Playground.tsx similarity index 100% rename from plugins/analytics-module-new-relic/dev/Playground.tsx rename to plugins/analytics-module-newrelic-browser/dev/Playground.tsx diff --git a/plugins/analytics-module-new-relic/dev/index.tsx b/plugins/analytics-module-newrelic-browser/dev/index.tsx similarity index 100% rename from plugins/analytics-module-new-relic/dev/index.tsx rename to plugins/analytics-module-newrelic-browser/dev/index.tsx diff --git a/plugins/analytics-module-new-relic/package.json b/plugins/analytics-module-newrelic-browser/package.json similarity index 100% rename from plugins/analytics-module-new-relic/package.json rename to plugins/analytics-module-newrelic-browser/package.json diff --git a/plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts similarity index 76% rename from plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts rename to plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts index 35be8cc8a0..d2c46d1394 100644 --- a/plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts +++ b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts @@ -107,7 +107,36 @@ export class NewRelicBrowser implements AnalyticsApi { } captureEvent(event: AnalyticsEvent) { - const { action, ...rest } = event; - this.agent.addPageAction(action, rest); + const { context, action, subject, value, attributes } = event; + if (action === 'navigate' && context.extension === 'App') { + const interaction = this.agent.interaction(); + interaction.setName(subject); + Object.keys(context).forEach(key => { + if (context[key]) { + interaction.setAttribute(`context.${key}`, context[key]); + } + }); + if (attributes) { + Object.keys(attributes).forEach(key => { + interaction.setAttribute(`attributes.${key}`, attributes[key]); + }); + } + } else { + const customAttributes: { + [x: string]: string | number | boolean | undefined; + } = {}; + Object.keys(context).forEach(key => { + if (context[key]) { + customAttributes[`context.${key}`] = context[key]; + } + }); + if (attributes) { + Object.keys(attributes).forEach(key => { + customAttributes[`attributes.${key}`] = attributes[key]; + }); + } + + this.agent.addPageAction(action, customAttributes); + } } } diff --git a/plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/index.ts b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/index.ts similarity index 100% rename from plugins/analytics-module-new-relic/src/apis/implementations/AnalyticsApi/index.ts rename to plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/index.ts diff --git a/plugins/analytics-module-new-relic/src/index.ts b/plugins/analytics-module-newrelic-browser/src/index.ts similarity index 100% rename from plugins/analytics-module-new-relic/src/index.ts rename to plugins/analytics-module-newrelic-browser/src/index.ts diff --git a/plugins/analytics-module-new-relic/src/setupTests.ts b/plugins/analytics-module-newrelic-browser/src/setupTests.ts similarity index 100% rename from plugins/analytics-module-new-relic/src/setupTests.ts rename to plugins/analytics-module-newrelic-browser/src/setupTests.ts diff --git a/yarn.lock b/yarn.lock index 89a006567c..c081af575d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4794,9 +4794,9 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-new-relic": +"@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-newrelic-browser": version: 0.0.0-use.local - resolution: "@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-new-relic" + resolution: "@backstage/plugin-analytics-module-newrelic-browser@workspace:plugins/analytics-module-newrelic-browser" dependencies: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" From 2a56c3d0753ce47272bb3f0d44fe300b7d038771 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 12 Jul 2023 14:57:54 +0200 Subject: [PATCH 049/329] Watching config change; update over MessageChannel Co-authored-by: Jack Palmer Co-authored-by: Patrik Oldsberg Co-authored-by: Vincenzo Scamporlino Signed-off-by: Philipp Hugenroth --- packages/cli/src/commands/start/startFrontend.ts | 6 ++++++ packages/cli/src/lib/bundler/config.ts | 10 ++++------ packages/cli/src/lib/bundler/server.ts | 13 +++++++++++++ packages/cli/src/lib/bundler/types.ts | 3 ++- packages/cli/src/lib/config.ts | 16 +++++++++++++++- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 00f7818b41..536b313dfe 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -91,11 +91,16 @@ export async function startFrontend(options: StartAppOptions) { checkReactVersion(); + const configChannel = new MessageChannel(); + const { name } = await fs.readJson(paths.resolveTarget('package.json')); const config = await loadCliConfig({ args: options.configPaths, fromPackage: name, withFilteredKeys: true, + watch(appConfigs) { + configChannel.port1.postMessage(appConfigs); + }, }); const appBaseUrl = config.frontendConfig.getString('app.baseUrl'); @@ -119,6 +124,7 @@ export async function startFrontend(options: StartAppOptions) { const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, + configChannel, ...config, }); diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 94b2a1c561..2f09a66e18 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -117,12 +117,6 @@ export async function createConfig( }), ); - plugins.push( - new webpack.EnvironmentPlugin({ - APP_CONFIG: options.frontendAppConfigs, - }), - ); - plugins.push( new HtmlWebpackPlugin({ template: paths.targetHtml, @@ -137,6 +131,10 @@ export async function createConfig( plugins.push( new webpack.DefinePlugin({ 'process.env.BUILD_INFO': JSON.stringify(buildInfo), + 'process.env.APP_CONFIG': webpack.DefinePlugin.runtimeValue( + () => JSON.stringify(options.getFrontendAppConfigs()), + true, + ), }), ); diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index cef55d3265..bef1a6fd7a 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -21,6 +21,7 @@ import openBrowser from 'react-dev-utils/openBrowser'; import { createConfig, resolveBaseUrl } from './config'; import { ServeOptions } from './types'; import { resolveBundlingPaths } from './paths'; +import { AppConfig } from '@backstage/config'; export async function serveBundle(options: ServeOptions) { const url = resolveBaseUrl(options.frontendConfig); @@ -32,6 +33,8 @@ export async function serveBundle(options: ServeOptions) { Number(url.port) || (url.protocol === 'https:' ? 443 : 80); + let latestFrontendAppConfigs = options.frontendAppConfigs; + const paths = resolveBundlingPaths(options); const pkgPath = paths.targetPackageJson; const pkg = await fs.readJson(pkgPath); @@ -39,6 +42,9 @@ export async function serveBundle(options: ServeOptions) { ...options, isDev: true, baseUrl: url, + getFrontendAppConfigs: () => { + return latestFrontendAppConfigs; + }, }); const compiler = webpack(config); @@ -95,6 +101,13 @@ export async function serveBundle(options: ServeOptions) { }); }); + options.configChannel.port2.onmessage = ({ + data, + }: MessageEvent) => { + latestFrontendAppConfigs = data; + server.invalidate(); + }; + const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 64916e349c..90fff4f3c1 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -22,7 +22,7 @@ export type BundlingOptions = { checksEnabled: boolean; isDev: boolean; frontendConfig: Config; - frontendAppConfigs: AppConfig[]; + getFrontendAppConfigs(): AppConfig[]; baseUrl: URL; parallelism?: number; }; @@ -32,6 +32,7 @@ export type ServeOptions = BundlingPathsOptions & { frontendConfig: Config; frontendAppConfigs: AppConfig[]; fullConfig: Config; + configChannel: MessageChannel; }; export type BuildOptions = BundlingPathsOptions & { diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index aaf3fcfcb9..b0efa8163a 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -19,7 +19,7 @@ import { loadConfig, loadConfigSchema, } from '@backstage/config-loader'; -import { ConfigReader } from '@backstage/config'; +import { AppConfig, ConfigReader } from '@backstage/config'; import { paths } from './paths'; import { isValidUrl } from './urls'; import { getPackages } from '@manypkg/get-packages'; @@ -33,6 +33,7 @@ type Options = { withDeprecatedKeys?: boolean; fullVisibility?: boolean; strict?: boolean; + watch?: (newFrontendAppConfigs: AppConfig[]) => void; }; export async function loadCliConfig(options: Options) { @@ -80,6 +81,19 @@ export async function loadCliConfig(options: Options) { : undefined, configRoot: paths.targetRoot, configTargets: configTargets, + watch: options.watch && { + onChange(newAppConfigs) { + const newFrontendAppConfigs = schema.process(newAppConfigs, { + visibility: options.fullVisibility + ? ['frontend', 'backend', 'secret'] + : ['frontend'], + withFilteredKeys: options.withFilteredKeys, + withDeprecatedKeys: options.withDeprecatedKeys, + ignoreSchemaErrors: !options.strict, + }); + options.watch!(newFrontendAppConfigs); + }, + }, }); // printing to stderr to not clobber stdout in case the cli command From 9910289122d9db70c052911d4ae313dae7a1ea96 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 16:29:37 +0200 Subject: [PATCH 050/329] Remove private flag Signed-off-by: Jonathan Mezach --- plugins/analytics-module-newrelic-browser/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/analytics-module-newrelic-browser/package.json b/plugins/analytics-module-newrelic-browser/package.json index 9364899003..f9243817f4 100644 --- a/plugins/analytics-module-newrelic-browser/package.json +++ b/plugins/analytics-module-newrelic-browser/package.json @@ -4,7 +4,6 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, "publishConfig": { "access": "public", "main": "dist/index.esm.js", From 8d47ae541e2a466484d089725519f69006b7523b Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 16:29:49 +0200 Subject: [PATCH 051/329] Send value to NR Browser as well Signed-off-by: Jonathan Mezach --- .../apis/implementations/AnalyticsApi/NewRelicBrowser.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts index d2c46d1394..274089d429 100644 --- a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts +++ b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts @@ -111,6 +111,9 @@ export class NewRelicBrowser implements AnalyticsApi { if (action === 'navigate' && context.extension === 'App') { const interaction = this.agent.interaction(); interaction.setName(subject); + if (value) { + interaction.setAttribute('value', value); + } Object.keys(context).forEach(key => { if (context[key]) { interaction.setAttribute(`context.${key}`, context[key]); @@ -125,6 +128,9 @@ export class NewRelicBrowser implements AnalyticsApi { const customAttributes: { [x: string]: string | number | boolean | undefined; } = {}; + if (value) { + customAttributes.value = value; + } Object.keys(context).forEach(key => { if (context[key]) { customAttributes[`context.${key}`] = context[key]; From 541418c1ff79762493ee28f993328bef7a9e64a0 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 16:36:04 +0200 Subject: [PATCH 052/329] Explicitly configure endpoints Signed-off-by: Jonathan Mezach --- .../analytics-module-newrelic-browser/README.md | 4 +++- .../config.d.ts | 12 ++++++------ .../AnalyticsApi/NewRelicBrowser.ts | 17 +++++------------ 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/plugins/analytics-module-newrelic-browser/README.md b/plugins/analytics-module-newrelic-browser/README.md index 98f17eae20..e31496505d 100644 --- a/plugins/analytics-module-newrelic-browser/README.md +++ b/plugins/analytics-module-newrelic-browser/README.md @@ -48,12 +48,14 @@ in New Relic Browser using the Copy/Paste method. app: analytics: nr: + endpoint: 'bam.nr-data.net', accountId: '1234567' applicationId: '987654321' licenseKey: 'NRJS-12a3456bc78de9123f4' - useEuEndpoint: false # Set this to true if you're using New Relic's EU data center ``` +> Note: Depending on New Relic's data center you are using you'll want to change the `endpoint` to `bam.eu01.nr-data.net` for the EU data center. Refer to [this document](https://docs.newrelic.com/docs/new-relic-solutions/get-started/networks/#data-ingest) for available endpoints. + ## Configuration By default the distributed tracing and cookies features are disabled. You can enable them by adding the following to your `app-config.yaml`: diff --git a/plugins/analytics-module-newrelic-browser/config.d.ts b/plugins/analytics-module-newrelic-browser/config.d.ts index e9c1b3ebca..22e99f3a15 100644 --- a/plugins/analytics-module-newrelic-browser/config.d.ts +++ b/plugins/analytics-module-newrelic-browser/config.d.ts @@ -18,6 +18,12 @@ export interface Config { app: { analytics?: { nr: { + /** + * Whether to use New Relic's EU Datacenter endpoints, defaults to false + * @visibility frontend + */ + endpoint: 'bam.eu01.nr-data.net' | 'bam.nr-data.net'; + /** * New Relic Account ID, e.g. 1234567 * @visibility frontend @@ -47,12 +53,6 @@ export interface Config { * @visibility frontend */ cookiesEnabled: boolean; - - /** - * Whether to use New Relic's EU Datacenter endpoints, defaults to false - * @visibility frontend - */ - useEuEndpoint: boolean; }; }; }; diff --git a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts index 274089d429..291d5eba68 100644 --- a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts +++ b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts @@ -25,12 +25,12 @@ import type { setAPI } from '@newrelic/browser-agent/loaders/api/api'; type NewRelicAPI = ReturnType; type NewRelicBrowserOptions = { + endpoint: string; accountId: string; applicationId: string; licenseKey: string; distributedTracingEnabled: boolean; cookiesEnabled: boolean; - useEuEndpoint: boolean; }; /** @@ -54,18 +54,12 @@ export class NewRelicBrowser implements AnalyticsApi { cookies_enabled: options.cookiesEnabled, }, ajax: { - deny_list: [ - options.useEuEndpoint ? 'bam.eu01.nr-data.net' : 'bam.nr-data.net', - ], + deny_list: [options.endpoint], }, }, info: { - beacon: options.useEuEndpoint - ? 'bam.eu01.nr-data.net' - : 'bam.nr-data.net', - errorBeacon: options.useEuEndpoint - ? 'bam.eu01.nr-data.net' - : 'bam.nr-data.net', + beacon: options.endpoint, + errorBeacon: options.endpoint, licenseKey: options.licenseKey, applicationID: options.applicationId, sa: 1, @@ -91,6 +85,7 @@ export class NewRelicBrowser implements AnalyticsApi { static fromConfig(config: Config, options: { identityApi?: IdentityApi }) { const browserOptions: NewRelicBrowserOptions = { + endpoint: config.getString('app.analytics.nr.endpoint'), accountId: config.getString('app.analytics.nr.accountId'), applicationId: config.getString('app.analytics.nr.applicationId'), licenseKey: config.getString('app.analytics.nr.licenseKey'), @@ -100,8 +95,6 @@ export class NewRelicBrowser implements AnalyticsApi { ) ?? false, cookiesEnabled: config.getOptionalBoolean('app.analytics.nr.cookiesEnabled') ?? false, - useEuEndpoint: - config.getOptionalBoolean('app.analytics.nr.useEuEndpoint') ?? false, }; return new NewRelicBrowser(browserOptions, options.identityApi); } From 9c87feae32c0e02b245f1f7bf9635eedc6e04c23 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 16:52:33 +0200 Subject: [PATCH 053/329] Add hashing of user ID's by default Signed-off-by: Jonathan Mezach --- .../README.md | 26 ++++++++++++ .../AnalyticsApi/NewRelicBrowser.ts | 41 +++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/plugins/analytics-module-newrelic-browser/README.md b/plugins/analytics-module-newrelic-browser/README.md index e31496505d..a94a60575e 100644 --- a/plugins/analytics-module-newrelic-browser/README.md +++ b/plugins/analytics-module-newrelic-browser/README.md @@ -74,6 +74,32 @@ app: This plugin supports sending user context to New Relic Browser by providing a User ID. This requires instantiating the `NewRelicBrowser` instance with an `identityApi` instance passed to it, but this is optional. If omitted the plugin will not send user context to New Relic Browser. +By default the user ID is calculated as a SHA-256 hash of the current user's `userEntityRef` as returned by the `identityApi`. To set a +different value, provide a `userIdTransform` function alongside `identityApi` when you instantiate `NewRelicBrowser`. This function will be passed the `userEntityRef` as an argument and should resolve to the value you wish to set as the user ID. For example: + +```typescript +import { + analyticsApiRef, + configApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { GoogleAnalytics } from '@backstage/plugin-analytics-module-newrelic-browser'; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: analyticsApiRef, + deps: { configApi: configApiRef, identityApi: identityApiRef }, + factory: ({ configApi, identityApi }) => + NewRelicBrowser.fromConfig(configApi, { + identityApi, + userIdTransform: async (userEntityRef: string): Promise => { + return customHashingFunction(userEntityRef); + }, + }), + }), +]; +``` + ## Development If you would like to contribute improvements to this plugin, the easiest way to diff --git a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts index 291d5eba68..293c0a9b45 100644 --- a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts +++ b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts @@ -43,6 +43,7 @@ export class NewRelicBrowser implements AnalyticsApi { private constructor( options: NewRelicBrowserOptions, identityApi?: IdentityApi, + userIdTransform?: 'sha-256' | ((userEntityRef: string) => Promise), ) { // Configure the New Relic Browser agent const agentOptions = { @@ -76,14 +77,31 @@ export class NewRelicBrowser implements AnalyticsApi { // Initialize the agent this.agent = new BrowserAgent(agentOptions) as unknown as NewRelicAPI; + // Check if identity has been provided if (identityApi) { identityApi.getBackstageIdentity().then(identity => { - this.agent.setUserId(identity.userEntityRef); + if (typeof userIdTransform === 'function') { + userIdTransform(identity.userEntityRef).then(userId => { + this.agent.setUserId(userId); + }); + } else { + this.hash(identity.userEntityRef).then(userId => { + this.agent.setUserId(userId); + }); + } }); } } - static fromConfig(config: Config, options: { identityApi?: IdentityApi }) { + static fromConfig( + config: Config, + options: { + identityApi?: IdentityApi; + userIdTransform?: + | 'sha-256' + | ((userEntityRef: string) => Promise); + }, + ) { const browserOptions: NewRelicBrowserOptions = { endpoint: config.getString('app.analytics.nr.endpoint'), accountId: config.getString('app.analytics.nr.accountId'), @@ -96,7 +114,11 @@ export class NewRelicBrowser implements AnalyticsApi { cookiesEnabled: config.getOptionalBoolean('app.analytics.nr.cookiesEnabled') ?? false, }; - return new NewRelicBrowser(browserOptions, options.identityApi); + return new NewRelicBrowser( + browserOptions, + options.identityApi, + options.userIdTransform, + ); } captureEvent(event: AnalyticsEvent) { @@ -138,4 +160,17 @@ export class NewRelicBrowser implements AnalyticsApi { this.agent.addPageAction(action, customAttributes); } } + + /** + * Simple hash function; relies on web cryptography + the sha-256 algorithm. + * @param value value to be hashed + */ + private async hash(value: string): Promise { + const digest = await window.crypto.subtle.digest( + 'sha-256', + new TextEncoder().encode(value), + ); + const hashArray = Array.from(new Uint8Array(digest)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } } From 82b8fc5bcf6e8bdba68f8149ff6c854a63fe1fbe Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 16:55:35 +0200 Subject: [PATCH 054/329] Add plugin to doc Signed-off-by: Jonathan Mezach --- docs/plugins/analytics.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 3e782b4460..c8f1c9b006 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -34,10 +34,11 @@ is a concrete implementation of [AnalyticsApi][analytics-api-type], common integrations are packaged and provided as plugins. Find your analytics tool of choice below. -| Analytics Tool | Support Status | -| ------------------------- | -------------- | -| [Google Analytics][ga] | Yes ✅ | -| [Google Analytics 4][ga4] | Yes ✅ | +| Analytics Tool | Support Status | +| ------------------------------------- | -------------- | +| [Google Analytics][ga] | Yes ✅ | +| [Google Analytics 4][ga4] | Yes ✅ | +| [New Relic Browser][newrelic-browser] | Community ✅ | To suggest an integration, please [open an issue][add-tool] for the analytics tool your organization uses. Or jump to [Writing Integrations][int-howto] to @@ -45,6 +46,7 @@ learn how to contribute the integration yourself! [ga]: https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md [ga4]: https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga4/README.md +[newrelic-browser]: https://github.com/backstage/backstage/blob/master/plugins/analytics-module-newrelic-browser/README.md [add-tool]: https://github.com/backstage/backstage/issues/new?assignees=&labels=plugin&template=plugin_template.md&title=%5BAnalytics+Module%5D+THE+ANALYTICS+TOOL+TO+INTEGRATE [int-howto]: #writing-integrations [analytics-api-type]: https://backstage.io/docs/reference/core-plugin-api.analyticsapi From 44fa2407236025e76e78fa18ce5a528f64a05917 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 19:03:32 +0200 Subject: [PATCH 055/329] Fix up API doc Signed-off-by: Jonathan Mezach --- plugins/analytics-module-newrelic-browser/api-report.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/analytics-module-newrelic-browser/api-report.md b/plugins/analytics-module-newrelic-browser/api-report.md index 26b478f64d..7556bae317 100644 --- a/plugins/analytics-module-newrelic-browser/api-report.md +++ b/plugins/analytics-module-newrelic-browser/api-report.md @@ -17,6 +17,9 @@ export class NewRelicBrowser implements AnalyticsApi { config: Config, options: { identityApi?: IdentityApi; + userIdTransform?: + | 'sha-256' + | ((userEntityRef: string) => Promise); }, ): NewRelicBrowser; } From 369b0948c9d823ec98e1d362ec46abe678dac21b Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 19:24:01 +0200 Subject: [PATCH 056/329] Fix reference to plugin in changeset Signed-off-by: Jonathan Mezach --- .changeset/rude-feet-sparkle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rude-feet-sparkle.md b/.changeset/rude-feet-sparkle.md index 12cb5fd1f6..fabc83bb0b 100644 --- a/.changeset/rude-feet-sparkle.md +++ b/.changeset/rude-feet-sparkle.md @@ -2,4 +2,4 @@ '@backstage/plugin-analytics-module-newrelic-browser': patch --- -Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-new-relic) for more details! +Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-newrelic-browser) for more details! From 8944880a2dd95012b80a319138d7f24a142e771c Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Wed, 12 Jul 2023 20:09:49 +0200 Subject: [PATCH 057/329] Process comments Signed-off-by: Jonathan Mezach --- .github/CODEOWNERS | 2 +- plugins/analytics-module-newrelic-browser/README.md | 2 +- plugins/analytics-module-newrelic-browser/config.d.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a2d5f4988a..5c957807cf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,7 +17,7 @@ /plugins/adr @backstage/maintainers @kuangp /plugins/adr-* @backstage/maintainers @kuangp /plugins/allure @backstage/maintainers @deepak-bhardwaj-ps -/plugins/analytics-module-nr @backstage/maintainers @jmezach +/plugins/analytics-module-newrelic-browser @backstage/maintainers @jmezach /plugins/apache-airflow @backstage/maintainers @cmpadden /plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers /plugins/azure-devops @backstage/maintainers @marleypowell @awanlin diff --git a/plugins/analytics-module-newrelic-browser/README.md b/plugins/analytics-module-newrelic-browser/README.md index a94a60575e..37c335d295 100644 --- a/plugins/analytics-module-newrelic-browser/README.md +++ b/plugins/analytics-module-newrelic-browser/README.md @@ -10,7 +10,7 @@ This plugin contains no other functionality. ```sh # From your Backstage root directory -yarn add --cwd packages/app @backstage/plugin-analytics-module-nr +yarn add --cwd packages/app @backstage/plugin-analytics-module-newrelic-browser ``` 2. Wire up the API implementation to your App: diff --git a/plugins/analytics-module-newrelic-browser/config.d.ts b/plugins/analytics-module-newrelic-browser/config.d.ts index 22e99f3a15..9f90e1cd8f 100644 --- a/plugins/analytics-module-newrelic-browser/config.d.ts +++ b/plugins/analytics-module-newrelic-browser/config.d.ts @@ -19,7 +19,7 @@ export interface Config { analytics?: { nr: { /** - * Whether to use New Relic's EU Datacenter endpoints, defaults to false + * Data ingestion endpoint to use, either bam.eu01.nr-data.net (EU) or bam.nr-data.net (US) * @visibility frontend */ endpoint: 'bam.eu01.nr-data.net' | 'bam.nr-data.net'; From 3b51b81408c7f24882f769c38090bfc820a5ba46 Mon Sep 17 00:00:00 2001 From: Chris James Date: Tue, 11 Jul 2023 13:29:25 -0700 Subject: [PATCH 058/329] Fix typo for UnprocessedEntitiesModule Signed-off-by: Chris James --- packages/backend/src/plugins/catalog.ts | 4 ++-- plugins/catalog-backend-module-unprocessed/README.md | 4 ++-- plugins/catalog-backend-module-unprocessed/api-report.md | 2 +- .../src/UnprocessedEntitiesModule.ts | 2 +- plugins/catalog-backend-module-unprocessed/src/module.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index a0fd21b4d8..7afd0a0cb7 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -19,7 +19,7 @@ import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backen import { Router } from 'express'; import { PluginEnvironment } from '../types'; import { DemoEventBasedEntityProvider } from './DemoEventBasedEntityProvider'; -import { UnprocessedEntitesModule } from '@backstage/plugin-catalog-backend-module-unprocessed'; +import { UnprocessedEntitiesModule } from '@backstage/plugin-catalog-backend-module-unprocessed'; export default async function createPlugin( env: PluginEnvironment, @@ -36,7 +36,7 @@ export default async function createPlugin( const { processingEngine, router } = await builder.build(); - const unprocessed = new UnprocessedEntitesModule( + const unprocessed = new UnprocessedEntitiesModule( await env.database.getClient(), router, ); diff --git a/plugins/catalog-backend-module-unprocessed/README.md b/plugins/catalog-backend-module-unprocessed/README.md index 92d9e59445..f2ea7b7741 100644 --- a/plugins/catalog-backend-module-unprocessed/README.md +++ b/plugins/catalog-backend-module-unprocessed/README.md @@ -19,11 +19,11 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-unproce In `packages/backend/src/plugins/catalog.ts` import the module and initialize it after invoking `CatalogBuilder.build()`: ```ts title="packages/backend/src/plugins/catalog.ts" -import { UnprocessedEntitesModule } from '@backstage/plugin-catalog-backend-module-unprocessed'; +import { UnprocessedEntitiesModule } from '@backstage/plugin-catalog-backend-module-unprocessed'; //... -const unprocessed = new UnprocessedEntitesModule( +const unprocessed = new UnprocessedEntitiesModule( await env.database.getClient(), router, ); diff --git a/plugins/catalog-backend-module-unprocessed/api-report.md b/plugins/catalog-backend-module-unprocessed/api-report.md index 3fc81c62cc..7fd7119276 100644 --- a/plugins/catalog-backend-module-unprocessed/api-report.md +++ b/plugins/catalog-backend-module-unprocessed/api-report.md @@ -11,7 +11,7 @@ import { Knex } from 'knex'; export const catalogModuleUnprocessedEntities: () => BackendFeature; // @public -export class UnprocessedEntitesModule { +export class UnprocessedEntitiesModule { constructor(database: Knex, router: HttpRouterService); // (undocumented) registerRoutes(): void; diff --git a/plugins/catalog-backend-module-unprocessed/src/UnprocessedEntitiesModule.ts b/plugins/catalog-backend-module-unprocessed/src/UnprocessedEntitiesModule.ts index 6ea184ce83..bdd3f64488 100644 --- a/plugins/catalog-backend-module-unprocessed/src/UnprocessedEntitiesModule.ts +++ b/plugins/catalog-backend-module-unprocessed/src/UnprocessedEntitiesModule.ts @@ -30,7 +30,7 @@ import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-no * * @public */ -export class UnprocessedEntitesModule { +export class UnprocessedEntitiesModule { private readonly moduleRouter; constructor( diff --git a/plugins/catalog-backend-module-unprocessed/src/module.ts b/plugins/catalog-backend-module-unprocessed/src/module.ts index 1c4b6c658f..1db4fd70b0 100644 --- a/plugins/catalog-backend-module-unprocessed/src/module.ts +++ b/plugins/catalog-backend-module-unprocessed/src/module.ts @@ -18,7 +18,7 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { UnprocessedEntitesModule } from './UnprocessedEntitiesModule'; +import { UnprocessedEntitiesModule } from './UnprocessedEntitiesModule'; /** * Catalog Module for Unprocessed Entities @@ -36,7 +36,7 @@ export const catalogModuleUnprocessedEntities = createBackendModule({ logger: coreServices.logger, }, async init({ database, router, logger }) { - const module = new UnprocessedEntitesModule( + const module = new UnprocessedEntitiesModule( await database.getClient(), router, ); From 5156a94c2e2a8f078fef9104370cd1ae64beecd3 Mon Sep 17 00:00:00 2001 From: Chris James Date: Wed, 12 Jul 2023 11:15:39 -0700 Subject: [PATCH 059/329] add changeset for typo Signed-off-by: Chris James --- .changeset/pink-squids-nail.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/pink-squids-nail.md diff --git a/.changeset/pink-squids-nail.md b/.changeset/pink-squids-nail.md new file mode 100644 index 0000000000..6645dfe2bf --- /dev/null +++ b/.changeset/pink-squids-nail.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-module-unprocessed': minor +'example-backend': minor +--- + +Fix typo of UnprocessedEntitiesModule. From 734cca679014cf70bc11ac47d4cfe0373b744c4d Mon Sep 17 00:00:00 2001 From: Chris James Date: Wed, 12 Jul 2023 11:23:01 -0700 Subject: [PATCH 060/329] Update pink-squids-nail.md Signed-off-by: Chris James --- .changeset/pink-squids-nail.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/pink-squids-nail.md b/.changeset/pink-squids-nail.md index 6645dfe2bf..ed95f3171a 100644 --- a/.changeset/pink-squids-nail.md +++ b/.changeset/pink-squids-nail.md @@ -1,6 +1,5 @@ --- '@backstage/plugin-catalog-backend-module-unprocessed': minor -'example-backend': minor --- Fix typo of UnprocessedEntitiesModule. From c3039c18061d9893cdd7fc0b15e06fb5cb1da1ae Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 05:43:53 +0200 Subject: [PATCH 061/329] cli: move config to buildBundle Signed-off-by: Vincenzo Scamporlino --- .../cli/src/commands/start/startFrontend.ts | 35 +--------- packages/cli/src/lib/bundler/bundle.ts | 1 + packages/cli/src/lib/bundler/server.ts | 66 ++++++++++++++----- packages/cli/src/lib/bundler/types.ts | 5 +- 4 files changed, 51 insertions(+), 56 deletions(-) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 536b313dfe..2cc951f591 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -import fs from 'fs-extra'; import chalk from 'chalk'; import uniq from 'lodash/uniq'; import { serveBundle } from '../../lib/bundler'; -import { loadCliConfig } from '../../lib/config'; import { PackageGraph } from '@backstage/cli-node'; import { Lockfile } from '../../lib/versioning'; import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint'; @@ -91,41 +89,10 @@ export async function startFrontend(options: StartAppOptions) { checkReactVersion(); - const configChannel = new MessageChannel(); - - const { name } = await fs.readJson(paths.resolveTarget('package.json')); - const config = await loadCliConfig({ - args: options.configPaths, - fromPackage: name, - withFilteredKeys: true, - watch(appConfigs) { - configChannel.port1.postMessage(appConfigs); - }, - }); - - const appBaseUrl = config.frontendConfig.getString('app.baseUrl'); - const backendBaseUrl = config.frontendConfig.getString('backend.baseUrl'); - if (appBaseUrl === backendBaseUrl) { - console.log( - chalk.yellow( - `⚠️ Conflict between app baseUrl and backend baseUrl: - - app.baseUrl: ${appBaseUrl} - backend.baseUrl: ${backendBaseUrl} - - Must have unique hostname and/or ports. - - This can be resolved by changing app.baseUrl and backend.baseUrl to point to their respective local development ports. -`, - ), - ); - } - const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, - configChannel, - ...config, + configPaths: options.configPaths, }); await waitForExit(); diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index b26c2f9f0d..7c45528c5c 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -45,6 +45,7 @@ export async function buildBundle(options: BuildOptions) { checksEnabled: false, isDev: false, baseUrl: resolveBaseUrl(options.frontendConfig), + getFrontendAppConfigs: () => options.frontendAppConfigs, }); const isCi = yn(process.env.CI, { default: false }); diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index bef1a6fd7a..2ac717243f 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -21,27 +21,64 @@ import openBrowser from 'react-dev-utils/openBrowser'; import { createConfig, resolveBaseUrl } from './config'; import { ServeOptions } from './types'; import { resolveBundlingPaths } from './paths'; +import { paths as libPaths } from '../../lib/paths'; +import { loadCliConfig } from '../config'; +import chalk from 'chalk'; import { AppConfig } from '@backstage/config'; export async function serveBundle(options: ServeOptions) { - const url = resolveBaseUrl(options.frontendConfig); + const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); + + let server: WebpackDevServer | undefined = undefined; + let latestFrontendAppConfigs: AppConfig[] = []; + + const cliConfig = await loadCliConfig({ + args: options.configPaths, + fromPackage: name, + withFilteredKeys: true, + watch(appConfigs) { + latestFrontendAppConfigs = appConfigs; + server?.invalidate(); + }, + }); + latestFrontendAppConfigs = cliConfig.frontendAppConfigs; + + const appBaseUrl = cliConfig.frontendConfig.getString('app.baseUrl'); + const backendBaseUrl = cliConfig.frontendConfig.getString('backend.baseUrl'); + if (appBaseUrl === backendBaseUrl) { + console.log( + chalk.yellow( + `⚠️ Conflict between app baseUrl and backend baseUrl: + + app.baseUrl: ${appBaseUrl} + backend.baseUrl: ${backendBaseUrl} + + Must have unique hostname and/or ports. + + This can be resolved by changing app.baseUrl and backend.baseUrl to point to their respective local development ports. +`, + ), + ); + } + + const { frontendConfig, fullConfig } = cliConfig; + const url = resolveBaseUrl(frontendConfig); const host = - options.frontendConfig.getOptionalString('app.listen.host') || url.hostname; + frontendConfig.getOptionalString('app.listen.host') || url.hostname; const port = - options.frontendConfig.getOptionalNumber('app.listen.port') || + frontendConfig.getOptionalNumber('app.listen.port') || Number(url.port) || (url.protocol === 'https:' ? 443 : 80); - let latestFrontendAppConfigs = options.frontendAppConfigs; - const paths = resolveBundlingPaths(options); const pkgPath = paths.targetPackageJson; const pkg = await fs.readJson(pkgPath); const config = await createConfig(paths, { - ...options, + checksEnabled: options.checksEnabled, isDev: true, baseUrl: url, + frontendConfig, getFrontendAppConfigs: () => { return latestFrontendAppConfigs; }, @@ -49,7 +86,7 @@ export async function serveBundle(options: ServeOptions) { const compiler = webpack(config); - const server = new WebpackDevServer( + server = new WebpackDevServer( { hot: !process.env.CI, devMiddleware: { @@ -73,8 +110,8 @@ export async function serveBundle(options: ServeOptions) { https: url.protocol === 'https:' ? { - cert: options.fullConfig.getString('app.https.certificate.cert'), - key: options.fullConfig.getString('app.https.certificate.key'), + cert: fullConfig.getString('app.https.certificate.cert'), + key: fullConfig.getString('app.https.certificate.key'), } : false, host, @@ -90,7 +127,7 @@ export async function serveBundle(options: ServeOptions) { ); await new Promise((resolve, reject) => { - server.startCallback((err?: Error) => { + server?.startCallback((err?: Error) => { if (err) { reject(err); return; @@ -101,17 +138,10 @@ export async function serveBundle(options: ServeOptions) { }); }); - options.configChannel.port2.onmessage = ({ - data, - }: MessageEvent) => { - latestFrontendAppConfigs = data; - server.invalidate(); - }; - const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { - server.close(); + server?.close(); // exit instead of resolve. The process is shutting down and resolving a promise here logs an error process.exit(); }); diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 90fff4f3c1..f96e4255f1 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -29,10 +29,7 @@ export type BundlingOptions = { export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; - frontendConfig: Config; - frontendAppConfigs: AppConfig[]; - fullConfig: Config; - configChannel: MessageChannel; + configPaths: string[]; }; export type BuildOptions = BundlingPathsOptions & { From 37491d9c531c8ebe014180e0a4addaaf21f11191 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 13:50:26 +0200 Subject: [PATCH 062/329] cli: move validation Co-Authored-by: Philipp Hugenroth Signed-off-by: Vincenzo Scamporlino --- .../cli/src/commands/start/startFrontend.ts | 64 ----------------- packages/cli/src/lib/bundler/server.ts | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 64 deletions(-) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index 2cc951f591..d7bdc3c384 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -import chalk from 'chalk'; -import uniq from 'lodash/uniq'; import { serveBundle } from '../../lib/bundler'; -import { PackageGraph } from '@backstage/cli-node'; -import { Lockfile } from '../../lib/versioning'; -import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint'; -import { paths } from '../../lib/paths'; interface StartAppOptions { verifyVersions?: boolean; @@ -30,65 +24,7 @@ interface StartAppOptions { configPaths: string[]; } -function checkReactVersion() { - try { - // Make sure we're looking at the root of the target repo - const reactPkgPath = require.resolve('react/package.json', { - paths: [paths.targetRoot], - }); - const reactPkg = require(reactPkgPath); - if (reactPkg.version.startsWith('16.')) { - console.log( - chalk.yellow( - ` -⚠️ ⚠️ -⚠️ You are using React version 16, which is deprecated for use in Backstage. ⚠️ -⚠️ Please upgrade to React 17 by updating your packages/app dependencies. ⚠️ -⚠️ ⚠️ -`, - ), - ); - } - } catch { - /* ignored */ - } -} - export async function startFrontend(options: StartAppOptions) { - if (options.verifyVersions) { - const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); - const result = lockfile.analyze({ - filter: includedFilter, - localPackages: PackageGraph.fromPackages( - await PackageGraph.listTargetPackages(), - ), - }); - const problemPackages = [...result.newVersions, ...result.newRanges] - .map(({ name }) => name) - .filter(forbiddenDuplicatesFilter); - - if (problemPackages.length > 1) { - console.log( - chalk.yellow( - `⚠️ Some of the following packages may be outdated or have duplicate installations: - - ${uniq(problemPackages).join(', ')} - `, - ), - ); - console.log( - chalk.yellow( - `⚠️ This can be resolved using the following command: - - yarn backstage-cli versions:check --fix - `, - ), - ); - } - } - - checkReactVersion(); - const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 2ac717243f..74c6b2a53c 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -18,6 +18,8 @@ import fs from 'fs-extra'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; import openBrowser from 'react-dev-utils/openBrowser'; +import uniq from 'lodash/uniq'; + import { createConfig, resolveBaseUrl } from './config'; import { ServeOptions } from './types'; import { resolveBundlingPaths } from './paths'; @@ -25,8 +27,50 @@ import { paths as libPaths } from '../../lib/paths'; import { loadCliConfig } from '../config'; import chalk from 'chalk'; import { AppConfig } from '@backstage/config'; +import { PackageGraph } from '@backstage/cli-node'; +import { Lockfile } from '../versioning'; +import { + forbiddenDuplicatesFilter, + includedFilter, +} from '../../commands/versions/lint'; export async function serveBundle(options: ServeOptions) { + if (options.verifyVersions) { + const lockfile = await Lockfile.load( + libPaths.resolveTargetRoot('yarn.lock'), + ); + const result = lockfile.analyze({ + filter: includedFilter, + localPackages: PackageGraph.fromPackages( + await PackageGraph.listTargetPackages(), + ), + }); + const problemPackages = [...result.newVersions, ...result.newRanges] + .map(({ name }) => name) + .filter(forbiddenDuplicatesFilter); + + if (problemPackages.length > 1) { + console.log( + chalk.yellow( + `⚠️ Some of the following packages may be outdated or have duplicate installations: + + ${uniq(problemPackages).join(', ')} + `, + ), + ); + console.log( + chalk.yellow( + `⚠️ This can be resolved using the following command: + + yarn backstage-cli versions:check --fix + `, + ), + ); + } + } + + checkReactVersion(); + const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); let server: WebpackDevServer | undefined = undefined; @@ -153,3 +197,27 @@ export async function serveBundle(options: ServeOptions) { return waitForExit; } + +function checkReactVersion() { + try { + // Make sure we're looking at the root of the target repo + const reactPkgPath = require.resolve('react/package.json', { + paths: [libPaths.targetRoot], + }); + const reactPkg = require(reactPkgPath); + if (reactPkg.version.startsWith('16.')) { + console.log( + chalk.yellow( + ` +⚠️ ⚠️ +⚠️ You are using React version 16, which is deprecated for use in Backstage. ⚠️ +⚠️ Please upgrade to React 17 by updating your packages/app dependencies. ⚠️ +⚠️ ⚠️ +`, + ), + ); + } + } catch { + /* ignored */ + } +} From 3f67cefb4780253bfa89e917089499101dba015f Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 13:54:39 +0200 Subject: [PATCH 063/329] cli: add changeset Co-Authored-by: Philipp Hugenroth Signed-off-by: Vincenzo Scamporlino --- .changeset/loud-garlics-press.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/loud-garlics-press.md diff --git a/.changeset/loud-garlics-press.md b/.changeset/loud-garlics-press.md new file mode 100644 index 0000000000..28aee20619 --- /dev/null +++ b/.changeset/loud-garlics-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Reload the frontend when app config changes From 77312229e82fa045693b7874c9c842495baef098 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 13:58:15 +0200 Subject: [PATCH 064/329] cli: add missing dependency Co-Authored-by: Philipp Hugenroth Signed-off-by: Vincenzo Scamporlino --- packages/cli/src/commands/start/startFrontend.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index d7bdc3c384..a809473788 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -29,6 +29,7 @@ export async function startFrontend(options: StartAppOptions) { entry: options.entry, checksEnabled: options.checksEnabled, configPaths: options.configPaths, + verifyVersions: options.verifyVersions, }); await waitForExit(); From cdf82c27102b5ff77374e864e9f5fe587443dd7d Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 13 Jul 2023 14:30:52 +0200 Subject: [PATCH 065/329] cli: safely invoke watch Signed-off-by: Vincenzo Scamporlino --- packages/cli/src/lib/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index b0efa8163a..0c657b640e 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -91,7 +91,7 @@ export async function loadCliConfig(options: Options) { withDeprecatedKeys: options.withDeprecatedKeys, ignoreSchemaErrors: !options.strict, }); - options.watch!(newFrontendAppConfigs); + options.watch?.(newFrontendAppConfigs); }, }, }); From 2acb7989993a4157db311bc303ef1349e66c565e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 18:35:29 +0000 Subject: [PATCH 066/329] Bump semver from 6.3.0 to 6.3.1 in /microsite Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/v6.3.1/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v6.3.1) --- updated-dependencies: - dependency-name: semver dependency-type: indirect ... Signed-off-by: dependabot[bot] --- microsite/yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/microsite/yarn.lock b/microsite/yarn.lock index e0f008bf9c..34bc7e6e12 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -10400,11 +10400,11 @@ __metadata: linkType: hard "semver@npm:^6.0.0, semver@npm:^6.1.1, semver@npm:^6.1.2, semver@npm:^6.3.0": - version: 6.3.0 - resolution: "semver@npm:6.3.0" + version: 6.3.1 + resolution: "semver@npm:6.3.1" bin: - semver: ./bin/semver.js - checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9 + semver: bin/semver.js + checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 languageName: node linkType: hard From f51d9776cb76a31edfa5086c632e23f8115e6363 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Jul 2023 15:28:56 +0000 Subject: [PATCH 067/329] chore(deps): update jamesives/github-pages-deploy-action action to v4.4.3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy_microsite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index 08d7c7c087..dddb2de4af 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -56,7 +56,7 @@ jobs: run: ls microsite/build && ls microsite/build/storybook - name: Deploy both microsite and storybook to gh-pages - uses: JamesIves/github-pages-deploy-action@v4.4.2 + uses: JamesIves/github-pages-deploy-action@v4.4.3 with: branch: gh-pages folder: microsite/build From ecf12e5de30dcc522e993f6755de7dc3af4d7cf9 Mon Sep 17 00:00:00 2001 From: alessandro Date: Fri, 7 Jul 2023 06:42:23 +0200 Subject: [PATCH 068/329] feat(catalog-backend-module-gitlab): Added option to skip forked repos in GitlabDiscoveryEntityProvider config Signed-off-by: alessandro --- docs/integrations/gitlab/discovery.md | 1 + plugins/catalog-backend-module-gitlab/src/lib/types.ts | 1 + .../src/providers/GitlabDiscoveryEntityProvider.ts | 7 +++++++ .../catalog-backend-module-gitlab/src/providers/config.ts | 3 +++ 4 files changed, 12 insertions(+) diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 4ddb0413b0..235d93ab03 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -23,6 +23,7 @@ catalog: host: gitlab-host # Identifies one of the hosts set up in the integrations branch: main # Optional. Used to discover on a specific branch fallbackBranch: main # Optional. Fallback to be used if there is no default branch configured at the Gitlab repository. It is only used, if `branch` is undefined. Uses `master` as default + skipForkedRepos: false # Optional. If the project is a fork, skip repository group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided patter. Defaults to `[\s\S]*`, which means to not filter anything diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index 341e2a9d9a..9b3a68c107 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -90,4 +90,5 @@ export type GitlabProviderConfig = { groupPattern: RegExp; orgEnabled?: boolean; schedule?: TaskScheduleDefinition; + skipForkedRepos?: boolean; }; diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 6001a32a4a..08d2fe48a7 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -177,6 +177,13 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { continue; } + if ( + this.config.skipForkedRepos && + project.hasOwnProperty('forked_from_project') + ) { + continue; + } + if ( !this.config.branch && this.config.fallbackBranch === '*' && diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index cd15c4825c..c34cb81808 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -43,6 +43,8 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { config.getOptionalString('groupPattern') ?? /[\s\S]*/, ); const orgEnabled: boolean = config.getOptionalBoolean('orgEnabled') ?? false; + const skipForkedRepos: boolean = + config.getOptionalBoolean('skipForkedRepos') ?? false; const schedule = config.has('schedule') ? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule')) @@ -60,6 +62,7 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { groupPattern, schedule, orgEnabled, + skipForkedRepos, }; } From e6c721439f37aeb723d121aed96dbde4f86284d9 Mon Sep 17 00:00:00 2001 From: alessandro Date: Fri, 7 Jul 2023 06:44:11 +0200 Subject: [PATCH 069/329] feat(catalog-backend-module-gitlab): Added changeset Signed-off-by: alessandro --- .changeset/khaki-flies-draw.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/khaki-flies-draw.md diff --git a/.changeset/khaki-flies-draw.md b/.changeset/khaki-flies-draw.md new file mode 100644 index 0000000000..ada45dea06 --- /dev/null +++ b/.changeset/khaki-flies-draw.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Added option to skip forked repos in GitlabDiscoveryEntityProvider From d913b37a8b32c7fcf72a79982c4e3157cda9807d Mon Sep 17 00:00:00 2001 From: alessandro Date: Fri, 7 Jul 2023 06:50:55 +0200 Subject: [PATCH 070/329] feat(catalog-backend-module-gitlab): Added tests Signed-off-by: alessandro --- .../GitlabDiscoveryEntityProvider.test.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts index 75a4c6414c..7508b35b84 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -343,6 +343,119 @@ describe('GitlabDiscoveryEntityProvider', () => { }); }); + it('should filter fork projects', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'test-gitlab', + apiBaseUrl: 'https://api.gitlab.example/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'test-gitlab', + skipForkedRepos: true, + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + server.use( + rest.get( + `https://api.gitlab.example/api/v4/projects`, + (_req, res, ctx) => { + const response = [ + { + id: 123, + default_branch: 'master', + archived: false, + last_activity_at: new Date().toString(), + web_url: 'https://api.gitlab.example/test-group/test-repo', + path_with_namespace: 'test-group/test-repo', + forked_from_project: { + id: 13083, + }, + }, + { + id: 124, + default_branch: 'master', + archived: false, + last_activity_at: new Date().toString(), + web_url: 'https://api.gitlab.example/john/example', + path_with_namespace: 'john/example', + }, + ]; + return res(ctx.json(response)); + }, + ), + rest.head( + 'https://api.gitlab.example/api/v4/projects/test-group%2Ftest-repo/repository/files/catalog-info.yaml', + (req, res, ctx) => { + if (req.url.searchParams.get('ref') === 'master') { + return res(ctx.status(200)); + } + return res(ctx.status(404, 'Not Found')); + }, + ), + rest.head( + 'https://api.gitlab.example/api/v4/projects/john%2Fexample/repository/files/catalog-info.yaml', + (req, res, ctx) => { + if (req.url.searchParams.get('ref') === 'master') { + return res(ctx.status(200)); + } + return res(ctx.status(404, 'Not Found')); + }, + ), + ); + + await provider.connect(entityProviderConnection); + + await provider.refresh(logger); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://api.gitlab.example/john/example/-/blob/master/catalog-info.yaml', + 'backstage.io/managed-by-origin-location': + 'url:https://api.gitlab.example/john/example/-/blob/master/catalog-info.yaml', + }, + name: 'generated-2045212e5b3e9e6bacf51cec709e362282e3cda9', + }, + spec: { + presence: 'optional', + target: + 'https://api.gitlab.example/john/example/-/blob/master/catalog-info.yaml', + type: 'url', + }, + }, + locationKey: 'GitlabDiscoveryEntityProvider:test-id', + }, + ], + }); + }); + it('fail without schedule and scheduler', () => { const config = new ConfigReader({ integrations: { From 0de20357eee659a8bfa7fef6fe50d8ba851da90f Mon Sep 17 00:00:00 2001 From: alessandro Date: Fri, 7 Jul 2023 07:05:33 +0200 Subject: [PATCH 071/329] feat(catalog-backend-module-gitlab): Fix tests Signed-off-by: alessandro --- .../src/providers/config.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index 1035538939..bac56cc6f3 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -59,6 +59,7 @@ describe('config', () => { userPattern: /[\s\S]*/, orgEnabled: false, schedule: undefined, + skipForkedRepos: false, }), ); }); @@ -95,6 +96,45 @@ describe('config', () => { userPattern: /[\s\S]*/, orgEnabled: false, schedule: undefined, + skipForkedRepos: false, + }), + ); + }); + + it('valid config with skipForkedRepos', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitlab: { + test: { + group: 'group', + host: 'host', + branch: 'not-master', + fallbackBranch: 'main', + entityFilename: 'custom-file.yaml', + skipForkedRepos: true, + }, + }, + }, + }, + }); + + const result = readGitlabConfigs(config); + expect(result).toHaveLength(1); + result.forEach(r => + expect(r).toStrictEqual({ + id: 'test', + group: 'group', + branch: 'not-master', + fallbackBranch: 'main', + host: 'host', + catalogFile: 'custom-file.yaml', + projectPattern: /[\s\S]*/, + groupPattern: /[\s\S]*/, + userPattern: /[\s\S]*/, + orgEnabled: false, + schedule: undefined, + skipForkedRepos: true, }), ); }); @@ -133,6 +173,7 @@ describe('config', () => { groupPattern: /[\s\S]*/, userPattern: /[\s\S]*/, orgEnabled: false, + skipForkedRepos: false, schedule: { frequency: Duration.fromISO('PT30M'), timeout: { From bf67dce73174e7c5087af7ed5d67b145ad75b105 Mon Sep 17 00:00:00 2001 From: ivgo Date: Tue, 18 Jul 2023 08:51:16 +0200 Subject: [PATCH 072/329] Make title a more optional parameter Signed-off-by: ivgo --- .changeset/mean-squids-relax.md | 5 +++++ plugins/home-react/api-report.md | 6 +++--- plugins/home-react/src/components/SettingsModal.tsx | 6 ++++-- plugins/home-react/src/extensions.tsx | 9 ++++----- plugins/home/README.md | 2 ++ plugins/home/api-report.md | 4 ++-- .../home/src/componentRenderers/ComponentAccordion.tsx | 2 +- 7 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 .changeset/mean-squids-relax.md diff --git a/.changeset/mean-squids-relax.md b/.changeset/mean-squids-relax.md new file mode 100644 index 0000000000..558b42fcfc --- /dev/null +++ b/.changeset/mean-squids-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home-react': patch +--- + +Make `title` optional when defining the `createCardExtension` diff --git a/plugins/home-react/api-report.md b/plugins/home-react/api-report.md index d6a2f92589..4806cf7be0 100644 --- a/plugins/home-react/api-report.md +++ b/plugins/home-react/api-report.md @@ -55,7 +55,7 @@ export type ComponentRenderer = { // @public export function createCardExtension(options: { - title: string; + title?: string; components: () => Promise; name?: string; description?: string; @@ -65,14 +65,14 @@ export function createCardExtension(options: { // @public (undocumented) export type RendererProps = { - title: string; + title?: string; } & ComponentParts; // @public (undocumented) export const SettingsModal: (props: { open: boolean; close: Function; - componentName: string; + componentName?: string; children: JSX.Element; }) => JSX.Element; ``` diff --git a/plugins/home-react/src/components/SettingsModal.tsx b/plugins/home-react/src/components/SettingsModal.tsx index ace9944f28..43cb16bb40 100644 --- a/plugins/home-react/src/components/SettingsModal.tsx +++ b/plugins/home-react/src/components/SettingsModal.tsx @@ -27,14 +27,16 @@ import { export const SettingsModal = (props: { open: boolean; close: Function; - componentName: string; + componentName?: string; children: JSX.Element; }) => { const { open, close, componentName, children } = props; return ( close()}> - Settings - {componentName} + + {componentName ? `Settings - ${componentName}` : 'Settings'} + {children}