fix: wrap child group transformer in try/catch to keep burst resilient

A throwing custom groupTransformer on a child group member would fail
the entire groups-phase burst. Apply the same try/catch + debug/warn
logging pattern already used for user member transformation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: pillaris <pillaris@adobe.com>
This commit is contained in:
pillaris
2026-04-27 15:43:33 +05:30
parent 69cf79ea00
commit 1b5e83401f
2 changed files with 86 additions and 7 deletions
@@ -728,6 +728,70 @@ describe('MicrosoftGraphIncrementalEntityProvider', () => {
expect(parentEntity!.entity.spec?.children).toHaveLength(0);
});
it('logs a warning and skips a child group member when the transformer throws', async () => {
const throwingGroupTransformer = jest
.fn()
.mockResolvedValueOnce({
// first call: parent group transform succeeds
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'parent-group',
annotations: { 'graph.microsoft.com/group-id': 'grp-parent' },
},
spec: { type: 'team', children: [], members: [] },
})
.mockRejectedValueOnce(new Error('Transformer error'));
(mockClient.getOrganization as jest.Mock).mockResolvedValue({
id: 'org-id',
displayName: 'My Org',
});
mockRequestOnePage.mockResolvedValueOnce({
items: [
{
id: 'grp-parent',
displayName: 'Parent',
mail: 'parent@example.com',
},
],
nextLink: undefined,
});
(mockClient.getGroupMembers as jest.Mock).mockReturnValue(
asyncYield({
'@odata.type': '#microsoft.graph.group',
id: 'grp-child',
displayName: 'Child Group',
mail: 'child@example.com',
}),
);
const provider = new MicrosoftGraphIncrementalEntityProvider({
id: 'default',
provider: baseProviderConfig as any,
logger,
groupTransformer: throwingGroupTransformer,
});
const result = await provider.next(makeContext(), groupsCursor);
// Parent group still emitted despite child transformer throwing
const parentEntity = result.entities!.find(
e =>
e.entity.metadata.annotations?.[
MICROSOFT_GRAPH_GROUP_ID_ANNOTATION
] === 'grp-parent',
);
expect(parentEntity).toBeDefined();
expect(parentEntity!.entity.spec?.children).toHaveLength(0);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
'group member child group grp-child failed to transform, skipping',
),
expect.anything(),
);
});
it('returns done:false with groups cursor when nextLink is present', async () => {
const nextLink =
'https://graph.microsoft.com/v1.0/groups?$skiptoken=page2';
@@ -481,14 +481,29 @@ export class MicrosoftGraphIncrementalEntityProvider
// With a filter, child groups may not be ingested themselves,
// which would produce dangling spec.children references.
if (!provider.groupFilter && !provider.groupSearch) {
const childEntity = await groupTransformer(
member as MicrosoftGraph.Group,
);
if (childEntity) {
childEntity.metadata.name = capEntityName(
childEntity.metadata.name,
try {
const childEntity = await groupTransformer(
member as MicrosoftGraph.Group,
);
if (childEntity) {
childEntity.metadata.name = capEntityName(
childEntity.metadata.name,
);
childRefs.push(stringifyEntityRef(childEntity));
} else {
this.options.logger.debug(
`${this.getProviderName()}: group member child group ${
member.id
} could not be transformed (sparse object?), skipping`,
);
}
} catch (e) {
this.options.logger.warn(
`${this.getProviderName()}: group member child group ${
member.id
} failed to transform, skipping`,
{ error: e },
);
childRefs.push(stringifyEntityRef(childEntity));
}
}
}