Skip to content

Commit 90bf896

Browse files
authored
fix: Sort private attributes for improved stable encoding (#401)
To help increase cache hits, we want to always use a stable encoding for the context. This was done in a previous commit when we added the `encoder.outputFormatting = [.sortedKeys]` modification. A context's private attributes are sorted as a set, which makes no guarantee about encoding order. To address this, we are sorting the private attributes prior to the JSON encoding phase. As a result, we would expect to see an increase in cache hits for customers.
1 parent dc3ebd6 commit 90bf896

File tree

2 files changed

+57
-1
lines changed

2 files changed

+57
-1
lines changed

LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ public struct LDContext: Encodable, Equatable {
9797
let includePrivateAttributes = encoder.userInfo[UserInfoKeys.includePrivateAttributes] as? Bool ?? false
9898

9999
if let privateAttributes = privateAttributes, !privateAttributes.isEmpty, includePrivateAttributes {
100-
try container.encodeIfPresent(privateAttributes, forKey: .privateAttributes)
100+
let sorted = privateAttributes.sorted { $0.raw() < $1.raw() }
101+
try container.encodeIfPresent(sorted, forKey: .privateAttributes)
101102
}
102103

103104
if let redactedAttributes = redactedAttributes, !redactedAttributes.isEmpty {

LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift

+55
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,63 @@ final class LDContextSpec: XCTestCase {
386386
}
387387
}
388388

389+
func testAttributeOrderDefinitionDoesNotAffectJsonEncoding() {
390+
var builder1 = LDContextBuilder(key: "context-key")
391+
builder1.kind("org")
392+
builder1.trySetValue("example-attribute", LDValue(stringLiteral: "Hi there"))
393+
builder1.name("Example name")
394+
builder1.addPrivateAttribute(Reference.init(literal: "first"))
395+
builder1.addPrivateAttribute(Reference.init(literal: "second"))
396+
builder1.addPrivateAttribute(Reference.init(literal: "third"))
397+
398+
guard case .success(let context1) = builder1.build()
399+
else {
400+
XCTFail("builder1.build should not have failed")
401+
return
402+
}
403+
404+
var builder2 = LDContextBuilder(key: "context-key")
405+
builder2.trySetValue("example-attribute", LDValue(stringLiteral: "Hi there"))
406+
builder2.name("Example name")
407+
builder2.kind("org")
408+
builder2.addPrivateAttribute(Reference.init(literal: "second"))
409+
builder2.addPrivateAttribute(Reference.init(literal: "third"))
410+
builder2.addPrivateAttribute(Reference.init(literal: "first"))
411+
412+
guard case .success(let context2) = builder2.build()
413+
else {
414+
XCTFail("builder1.build should not have failed")
415+
return
416+
}
417+
418+
let encoder = JSONEncoder()
419+
encoder.userInfo[LDContext.UserInfoKeys.includePrivateAttributes] = true
420+
encoder.userInfo[LDContext.UserInfoKeys.redactAttributes] = false
421+
encoder.outputFormatting = [.sortedKeys]
422+
423+
guard let context1JsonData = try? encoder.encode(context1)
424+
else {
425+
XCTFail("failed to encode context1")
426+
return
427+
}
428+
429+
guard let context2JsonData = try? encoder.encode(context2)
430+
else {
431+
XCTFail("failed to encode context2")
432+
return
433+
}
434+
435+
XCTAssertEqual(context1JsonData, context2JsonData)
436+
}
437+
389438
func testAttributeOrderDefinitionDoesNotAffectContextHash() {
390439
var builder1 = LDContextBuilder(key: "context-key")
391440
builder1.kind("org")
392441
builder1.trySetValue("example-attribute", LDValue(stringLiteral: "Hi there"))
393442
builder1.name("Example name")
443+
builder1.addPrivateAttribute(Reference.init(literal: "first"))
444+
builder1.addPrivateAttribute(Reference.init(literal: "second"))
445+
builder1.addPrivateAttribute(Reference.init(literal: "third"))
394446

395447
guard case .success(let context1) = builder1.build()
396448
else {
@@ -402,6 +454,9 @@ final class LDContextSpec: XCTestCase {
402454
builder2.trySetValue("example-attribute", LDValue(stringLiteral: "Hi there"))
403455
builder2.name("Example name")
404456
builder2.kind("org")
457+
builder2.addPrivateAttribute(Reference.init(literal: "second"))
458+
builder2.addPrivateAttribute(Reference.init(literal: "third"))
459+
builder2.addPrivateAttribute(Reference.init(literal: "first"))
405460

406461
guard case .success(let context2) = builder2.build()
407462
else {

0 commit comments

Comments
 (0)