Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 65b9b9a

Browse files
monicatangfacebook-github-bot
authored andcommitted
Log ID collisions in production
Reviewed By: captbaritone Differential Revision: D70283726 fbshipit-source-id: 3a4d0ba1ff1b7a292d2a25b12c0f5fca81c123f7
1 parent 8637294 commit 65b9b9a

File tree

6 files changed

+428
-59
lines changed

6 files changed

+428
-59
lines changed

packages/relay-runtime/store/RelayResponseNormalizer.js

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -508,18 +508,16 @@ class RelayResponseNormalizer {
508508
return;
509509
}
510510
}
511-
if (__DEV__) {
512-
if (selection.kind === 'ScalarField') {
513-
this._validateConflictingFieldsWithIdenticalId(
514-
record,
515-
storageKey,
516-
// When using `treatMissingFieldsAsNull` the conflicting validation raises a false positive
517-
// because the value is set using `null` but validated using `fieldValue` which at this point
518-
// will be `undefined`.
519-
// Setting this to `null` matches the value that we actually set to the `fieldValue`.
520-
null,
521-
);
522-
}
511+
if (selection.kind === 'ScalarField') {
512+
this._validateConflictingFieldsWithIdenticalId(
513+
record,
514+
storageKey,
515+
// When using `treatMissingFieldsAsNull` the conflicting validation raises a false positive
516+
// because the value is set using `null` but validated using `fieldValue` which at this point
517+
// will be `undefined`.
518+
// Setting this to `null` matches the value that we actually set to the `fieldValue`.
519+
null,
520+
);
523521
}
524522
if (isNoncompliantlyNullish) {
525523
// We need to preserve the fact that we received an empty list
@@ -542,13 +540,11 @@ class RelayResponseNormalizer {
542540
}
543541

544542
if (selection.kind === 'ScalarField') {
545-
if (__DEV__) {
546-
this._validateConflictingFieldsWithIdenticalId(
547-
record,
548-
storageKey,
549-
fieldValue,
550-
);
551-
}
543+
this._validateConflictingFieldsWithIdenticalId(
544+
record,
545+
storageKey,
546+
fieldValue,
547+
);
552548
RelayModernRecord.setValue(record, storageKey, fieldValue);
553549
} else if (selection.kind === 'LinkedField') {
554550
this._path.push(responseKey);
@@ -692,21 +688,19 @@ class RelayResponseNormalizer {
692688
'RelayResponseNormalizer: Expected id on field `%s` to be a string.',
693689
storageKey,
694690
);
695-
if (__DEV__) {
696-
this._validateConflictingLinkedFieldsWithIdenticalId(
697-
RelayModernRecord.getLinkedRecordID(record, storageKey),
698-
nextID,
699-
storageKey,
700-
);
701-
}
691+
this._validateConflictingLinkedFieldsWithIdenticalId(
692+
RelayModernRecord.getLinkedRecordID(record, storageKey),
693+
nextID,
694+
storageKey,
695+
);
702696
RelayModernRecord.setLinkedRecordID(record, storageKey, nextID);
703697
let nextRecord = this._recordSource.get(nextID);
704698
if (!nextRecord) {
705699
// $FlowFixMe[incompatible-variance]
706700
const typeName = field.concreteType || this._getRecordType(fieldValue);
707701
nextRecord = RelayModernRecord.create(nextID, typeName);
708702
this._recordSource.set(nextID, nextRecord);
709-
} else if (__DEV__) {
703+
} else {
710704
this._validateRecordType(nextRecord, field, fieldValue);
711705
}
712706
// $FlowFixMe[incompatible-variance]
@@ -772,19 +766,15 @@ class RelayResponseNormalizer {
772766
const typeName = field.concreteType || this._getRecordType(item);
773767
nextRecord = RelayModernRecord.create(nextID, typeName);
774768
this._recordSource.set(nextID, nextRecord);
775-
} else if (__DEV__) {
769+
} else {
776770
this._validateRecordType(nextRecord, field, item);
777771
}
778-
// NOTE: the check to strip __DEV__ code only works for simple
779-
// `if (__DEV__)`
780-
if (__DEV__) {
781-
if (prevIDs) {
782-
this._validateConflictingLinkedFieldsWithIdenticalId(
783-
prevIDs[nextIndex],
784-
nextID,
785-
storageKey,
786-
);
787-
}
772+
if (prevIDs) {
773+
this._validateConflictingLinkedFieldsWithIdenticalId(
774+
prevIDs[nextIndex],
775+
nextID,
776+
storageKey,
777+
);
788778
}
789779
// $FlowFixMe[incompatible-variance]
790780
this._traverseSelections(field, nextRecord, item);
@@ -802,20 +792,36 @@ class RelayResponseNormalizer {
802792
field: NormalizationLinkedField,
803793
payload: Object,
804794
): void {
805-
const typeName = field.concreteType ?? this._getRecordType(payload);
806-
const dataID = RelayModernRecord.getDataID(record);
807-
warning(
808-
(isClientID(dataID) && dataID !== ROOT_ID) ||
809-
RelayModernRecord.getType(record) === typeName,
810-
'RelayResponseNormalizer: Invalid record `%s`. Expected %s to be ' +
811-
'consistent, but the record was assigned conflicting types `%s` ' +
812-
'and `%s`. The GraphQL server likely violated the globally unique ' +
813-
'id requirement by returning the same id for different objects.',
814-
dataID,
815-
TYPENAME_KEY,
816-
RelayModernRecord.getType(record),
817-
typeName,
818-
);
795+
const log = RelayFeatureFlags.LOG_STORE_ID_COLLISION;
796+
if (log) {
797+
const typeName = field.concreteType ?? this._getRecordType(payload);
798+
const dataID = RelayModernRecord.getDataID(record);
799+
const shouldLogWarning =
800+
(isClientID(dataID) && dataID !== ROOT_ID) ||
801+
RelayModernRecord.getType(record) === typeName;
802+
if (shouldLogWarning) {
803+
log({name: 'idCollision.typename'});
804+
}
805+
}
806+
// NOTE: Only emit a warning in DEV
807+
if (__DEV__) {
808+
const typeName = field.concreteType ?? this._getRecordType(payload);
809+
const dataID = RelayModernRecord.getDataID(record);
810+
const shouldLogWarning =
811+
(isClientID(dataID) && dataID !== ROOT_ID) ||
812+
RelayModernRecord.getType(record) === typeName;
813+
warning(
814+
shouldLogWarning,
815+
'RelayResponseNormalizer: Invalid record `%s`. Expected %s to be ' +
816+
'consistent, but the record was assigned conflicting types `%s` ' +
817+
'and `%s`. The GraphQL server likely violated the globally unique ' +
818+
'id requirement by returning the same id for different objects.',
819+
dataID,
820+
TYPENAME_KEY,
821+
RelayModernRecord.getType(record),
822+
typeName,
823+
);
824+
}
819825
}
820826

821827
/**
@@ -826,14 +832,27 @@ class RelayResponseNormalizer {
826832
storageKey: string,
827833
fieldValue: mixed,
828834
): void {
829-
// NOTE: Only call this function in DEV
835+
const log = RelayFeatureFlags.LOG_STORE_ID_COLLISION;
836+
if (log) {
837+
const previousValue = RelayModernRecord.getValue(record, storageKey);
838+
const shouldLogWarning =
839+
storageKey === TYPENAME_KEY ||
840+
previousValue === undefined ||
841+
areEqual(previousValue, fieldValue);
842+
if (shouldLogWarning) {
843+
log({name: 'idCollision.field'});
844+
}
845+
}
846+
// NOTE: Only emit a warning in DEV
830847
if (__DEV__) {
848+
const previousValue = RelayModernRecord.getValue(record, storageKey);
831849
const dataID = RelayModernRecord.getDataID(record);
832-
var previousValue = RelayModernRecord.getValue(record, storageKey);
833-
warning(
850+
const shouldLogWarning =
834851
storageKey === TYPENAME_KEY ||
835-
previousValue === undefined ||
836-
areEqual(previousValue, fieldValue),
852+
previousValue === undefined ||
853+
areEqual(previousValue, fieldValue);
854+
warning(
855+
shouldLogWarning,
837856
'RelayResponseNormalizer: Invalid record. The record contains two ' +
838857
'instances of the same id: `%s` with conflicting field, %s and its values: %s and %s. ' +
839858
'If two fields are different but share ' +
@@ -854,10 +873,18 @@ class RelayResponseNormalizer {
854873
nextID: DataID,
855874
storageKey: string,
856875
): void {
857-
// NOTE: Only call this function in DEV
876+
const log = RelayFeatureFlags.LOG_STORE_ID_COLLISION;
877+
if (log) {
878+
const shouldLogWarning = prevID === undefined || prevID === nextID;
879+
if (shouldLogWarning) {
880+
log({name: 'idCollision.field'});
881+
}
882+
}
883+
// NOTE: Only emit a warning in DEV
858884
if (__DEV__) {
885+
const shouldLogWarning = prevID === undefined || prevID === nextID;
859886
warning(
860-
prevID === undefined || prevID === nextID,
887+
shouldLogWarning,
861888
'RelayResponseNormalizer: Invalid record. The record contains ' +
862889
'references to the conflicting field, %s and its id values: %s and %s. ' +
863890
'We need to make sure that the record the field points ' +

packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4639,4 +4639,112 @@ describe('RelayResponseNormalizer', () => {
46394639
});
46404640
});
46414641
});
4642+
4643+
describe('Test ID collision logging', () => {
4644+
let logCount = 0;
4645+
beforeEach(() => {
4646+
logCount = 0;
4647+
RelayFeatureFlags.LOG_STORE_ID_COLLISION = () => {
4648+
logCount += 1;
4649+
};
4650+
});
4651+
4652+
it('warns when normalizing payloads with same id but different types', () => {
4653+
const me = graphql`
4654+
query RelayResponseNormalizerTestError1Query {
4655+
me {
4656+
id
4657+
__typename
4658+
}
4659+
}
4660+
`;
4661+
const payload1 = {
4662+
me: {
4663+
id: '1',
4664+
__typename: 'User',
4665+
},
4666+
};
4667+
4668+
const recordSource = new RelayRecordSource();
4669+
recordSource.set(ROOT_ID, RelayModernRecord.create(ROOT_ID, ROOT_TYPE));
4670+
normalize(
4671+
recordSource,
4672+
createNormalizationSelector(me.operation, ROOT_ID, {
4673+
id: '1',
4674+
}),
4675+
payload1,
4676+
defaultOptions,
4677+
);
4678+
expect(recordSource.toJSON()).toMatchSnapshot();
4679+
const payload2 = {
4680+
me: {
4681+
id: '1',
4682+
__typename: 'Cat',
4683+
},
4684+
};
4685+
expectToWarn(
4686+
'RelayModernRecord: Invalid field update, expected both versions of record `1` to have the same `__typename` but got conflicting types `User` and `Cat`. The GraphQL server likely violated the globally unique id requirement by returning the same id for different objects.',
4687+
() => {
4688+
normalize(
4689+
recordSource,
4690+
createNormalizationSelector(me.operation, ROOT_ID, {
4691+
id: '1',
4692+
}),
4693+
payload2,
4694+
defaultOptions,
4695+
);
4696+
},
4697+
);
4698+
expect(logCount).toBeGreaterThan(0);
4699+
});
4700+
4701+
it('warns when normalizing payloads with same id but different fields', () => {
4702+
const me = graphql`
4703+
query RelayResponseNormalizerTestError2Query {
4704+
me {
4705+
id
4706+
lastName
4707+
}
4708+
}
4709+
`;
4710+
const payload1 = {
4711+
me: {
4712+
id: '1',
4713+
lastName: 'Hanks',
4714+
},
4715+
};
4716+
4717+
const recordSource = new RelayRecordSource();
4718+
recordSource.set(ROOT_ID, RelayModernRecord.create(ROOT_ID, ROOT_TYPE));
4719+
normalize(
4720+
recordSource,
4721+
createNormalizationSelector(me.operation, ROOT_ID, {
4722+
id: '1',
4723+
}),
4724+
payload1,
4725+
defaultOptions,
4726+
);
4727+
expect(recordSource.toJSON()).toMatchSnapshot();
4728+
const payload2 = {
4729+
me: {
4730+
id: '1',
4731+
lastName: 'Cyrus',
4732+
},
4733+
};
4734+
expectToWarn(
4735+
'RelayResponseNormalizer: Invalid record. The record contains two instances of the same id: `1` with conflicting field, lastName and its values: Hanks and Cyrus. If two fields are different but share the same id, one field will overwrite the other.',
4736+
() => {
4737+
normalize(
4738+
recordSource,
4739+
createNormalizationSelector(me.operation, ROOT_ID, {
4740+
id: '1',
4741+
}),
4742+
payload2,
4743+
defaultOptions,
4744+
);
4745+
},
4746+
);
4747+
expect(logCount).toBeGreaterThan(0);
4748+
});
4749+
});
46424750
});

0 commit comments

Comments
 (0)