Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. See [standa

### Features

* Added segment management methods to `ChatClient`: `CreateSegment`, `UpdateSegment`, and `AddSegmentTargets`. Segments targeted by a campaign can now be created and edited through the SDK, completing the campaign workflow without falling back to raw REST ([CHA-3483](https://linear.app/stream/issue/CHA-3483)).
* Standardized error handling per the Server-Side SDK Error Handling Spec ([CHA-2958](https://linear.app/stream/issue/CHA-2958)).
* Four sentinel error categories on the existing `*StreamError`: `ErrApiResponse`, `ErrRateLimited`, `ErrTransport`, `ErrTaskFailed`. Use `errors.Is(err, ...)` to branch and `errors.As(err, &streamErr)` to extract typed fields. `ErrRateLimited` also satisfies `errors.Is(err, ErrApiResponse)`.
* New fields on `StreamError`: `Unrecoverable`, `Details`, `RawResponseBody`, `RetryAfter`, `ErrorType`, `Task`. No existing field accesses change.
Expand Down
27 changes: 27 additions & 0 deletions chat.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 78 additions & 0 deletions chat_misc_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1875,3 +1875,81 @@ func TestChatContextExceededIntegration(t *testing.T) {
require.Error(t, err)
assert.ErrorIs(t, err, context.DeadlineExceeded)
}

// TestChatSegmentCampaignIntegration exercises the full campaign flow that
// requires the segment management endpoints: create a segment, add targets,
// create a campaign that targets it, then start and stop it. Skips when the
// app does not have the campaigns feature enabled.
func TestChatSegmentCampaignIntegration(t *testing.T) {
t.Parallel()
skipIfShort(t)
client := initClient(t)
ctx := context.Background()

userIDs := createTestUsers(t, client, 2)
senderID, targetID := userIDs[0], userIDs[1]

segmentID := "test-segment-" + randomString(12)
createResp, err := client.Chat().CreateSegment(ctx, &CreateSegmentRequest{
Type: "user",
ID: PtrTo(segmentID),
Name: PtrTo("integration test segment"),
})
if err != nil && (strings.Contains(err.Error(), "not enabled") || strings.Contains(err.Error(), "not available")) {
t.Skip("Campaigns feature not enabled for this app")
}
require.NoError(t, err)
require.NotNil(t, createResp.Data.Segment)
require.Equal(t, segmentID, createResp.Data.Segment.ID)

t.Cleanup(func() {
_, _ = client.Chat().DeleteSegment(context.Background(), segmentID, &DeleteSegmentRequest{})
})

// Add the target user, then confirm it is in the segment.
_, err = client.Chat().AddSegmentTargets(ctx, segmentID, &AddSegmentTargetsRequest{
TargetIds: []string{targetID},
})
require.NoError(t, err)

_, err = client.Chat().SegmentTargetExists(ctx, segmentID, targetID, &SegmentTargetExistsRequest{})
require.NoError(t, err)

// Update the segment description through the newly exposed endpoint.
_, err = client.Chat().UpdateSegment(ctx, segmentID, &UpdateSegmentRequest{
Description: PtrTo("updated by integration test"),
})
require.NoError(t, err)

// Create a campaign targeting the segment, then start and stop it.
campaignResp, err := client.Chat().CreateCampaign(ctx, &CreateCampaignRequest{
SenderID: senderID,
SegmentIds: []string{segmentID},
CreateChannels: PtrTo(true),
MessageTemplate: CampaignMessageTemplate{Text: "hello from integration test"},
// channel_template is required by the backend when create_channels is true.
ChannelTemplate: &CampaignChannelTemplate{
Type: "messaging",
ID: PtrTo("{{receiver.id}}-{{sender.id}}"),
},
})
require.NoError(t, err)
require.NotNil(t, campaignResp.Data.Campaign)
campaignID := campaignResp.Data.Campaign.ID

t.Cleanup(func() {
_, _ = client.Chat().DeleteCampaign(context.Background(), campaignID, &DeleteCampaignRequest{})
})

// Schedule for the future so the campaign stays in "scheduled" state, then
// stop it. A campaign started immediately could complete before the stop
// call lands, and a completed campaign cannot be stopped.
scheduledFor := time.Now().Add(time.Hour)
_, err = client.Chat().StartCampaign(ctx, campaignID, &StartCampaignRequest{
ScheduledFor: PtrTo(Timestamp{Time: &scheduledFor}),
})
require.NoError(t, err)

_, err = client.Chat().StopCampaign(ctx, campaignID, &StopCampaignRequest{})
require.NoError(t, err)
}
21 changes: 21 additions & 0 deletions chat_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions requests.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading