Skip to content

Complex example #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ for unit tests using Angular 5.
- `npm install -g @angular/cli`
- `npm start` it will start a server on `localhost:4200`

- Update your relevant Split API Key, Track type and Split names in: ./src/app/splitio.service.ts
- Lookup the following section for SDK API and Track type:
private _initSdk(): void {
this._factory = SplitFactory({
core: {
authorizationKey: '[API KEY here]',
key: 'Tracke type'
},
- If the second Split has different Traffic Type, add the second traffic type here:
// and the one for a different key. This will share resources with the main one but segment data.
this._splitAccountClient = this._factory.client('[Second Traffic Type]');

- Lookup the line below and update the Split names needed:
userFeatures: string[] = [
'[Split1]', '[Split2]'



## Notes

Used example code from `https://angular.io/guide/quickstart`.
18 changes: 12 additions & 6 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@angular/platform-server": "~5.0.0",
"@angular/router": "~5.0.0",
"@angular/upgrade": "~5.0.0",
"@splitsoftware/splitio": "^10.2.0",
"@splitsoftware/splitio": "^10.3.0",
"core-js": "^2.4.1",
"rxjs": "^5.5.0",
"zone.js": "^0.8.4"
Expand Down
21 changes: 21 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { FeaturesComponent } from './features/features.component';

// We'll only resolve Split here but we could potentially
// use it for other prerrequisite data.
import { AppResolver } from './app.resolver';

const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: FeaturesComponent, resolve: {
prerrequisitesReady: AppResolver
}}
];

@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
3 changes: 2 additions & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<h1>{{title}}</h1>
<app-features></app-features>
<!-- Here's where our actual page will be -->
<router-outlet></router-outlet>
2 changes: 1 addition & 1 deletion src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import { Component } from '@angular/core';
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Example App Angular';
title = 'App component is kind of dumb, used for root <router-outlet>';
}
10 changes: 8 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

import { AppRoutingModule } from './app-routing.module';

// splitio sdk
import { SplitioService } from './splitio.service';
import { AppResolver } from './app.resolver';
import { FeaturesComponent } from './features/features.component';

@NgModule({
imports: [
BrowserModule,
FormsModule
FormsModule,
AppRoutingModule
],
declarations: [
AppComponent,
FeaturesComponent
],
providers: [
providers: [
// Import the resolver
AppResolver,
SplitioService
],
bootstrap: [ AppComponent ]
Expand Down
15 changes: 15 additions & 0 deletions src/app/app.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { SplitioService } from './splitio.service';

@Injectable()
export class AppResolver implements Resolve<any> {
// Inject the service on the resolver.
constructor(private splitService: SplitioService) {}

resolve() {
// Use it on the resolve function and return a promise
// for the ready.
return this.splitService.getReadyPromise();
}
}
7 changes: 7 additions & 0 deletions src/app/features/features.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.features__treatments-container {
margin-top: 20px;
}

.features__treatments-item {
margin: 10px 0;
}
9 changes: 4 additions & 5 deletions src/app/features/features.component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<h2>Features</h2>
<button (click)="splitioService.getTreatments()">Get Treatments</button>
<div *ngFor="let feature of splitioService.features">
<div *ngIf="splitioService.treatments && splitioService.treatments[feature]">
<section class="features__treatments-container" >
<div *ngFor="let feature of featuresList; trackBy: trackByFlagChanges" class="features__treatments-item" >
<strong>{{feature}}: </strong>
{{splitioService.treatments[feature]}}
{{treatments[feature]}}
</div>
</div>
</section>
30 changes: 24 additions & 6 deletions src/app/features/features.component.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import { Component, OnInit } from '@angular/core';
import { Component, ChangeDetectorRef } from '@angular/core';
import { SplitioService } from '../splitio.service';

@Component({
selector: 'app-features',
templateUrl: './features.component.html',
styleUrls: ['./features.component.css']
})
export class FeaturesComponent implements OnInit {
// We're not adding much code here. We're handling the display on the template.
export class FeaturesComponent {

treatments: SplitIO.Treatments
treatments: SplitIO.Treatments;
featuresList: string[];

constructor(public splitioService: SplitioService) { }
constructor(private _splitioService: SplitioService, private _ref: ChangeDetectorRef) {
this._refreshData(this._splitioService.treatments);

ngOnInit() {
this.splitioService.initSdk();
// Subscribe to the observable. You could use the static list too.
this._splitioService.treatmentsObs.subscribe(
flags => {
this._refreshData(flags);
this._ref.detectChanges();
}
);
}

private _refreshData(flags) {
this.treatments = { ...flags };
this.featuresList = Object.keys(flags);
}

trackByFlagChanges = (i, item) => {
const treatment = this.treatments[item];
return `${item}-${treatment}`;
}
}
140 changes: 103 additions & 37 deletions src/app/splitio.service.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,127 @@
import { Injectable } from '@angular/core';
import { SplitFactory } from '@splitsoftware/splitio';
import { fromEvent } from 'rxjs/observable/fromEvent';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class SplitioService {

splitio: SplitIO.ISDK;
splitClient: SplitIO.IClient;
isReady: boolean = false;
treatments: SplitIO.Treatments
features: string[] = [
'feature_1',
'feature_2',
'feature_3'
private _isSdkReady: boolean = null;
private _factory: SplitIO.ISDK;
private _splitUserClient: SplitIO.IClient;
private _splitAccountClient: SplitIO.IClient;
private _readyPromise: Promise<boolean>;

// If you use an observable is easier to react to updates, so showing both cases.
private _treatmentsSubject: Subject<SplitIO.Treatments> = new Subject();

treatmentsObs: Observable<SplitIO.Treatments> = this._treatmentsSubject.asObservable();
// You'll probably store your treatments somewhere else, but it's not
// a bad idea to have those in a single source of truth and just consuming it from elsewhere.
treatments: SplitIO.Treatments = {};
// This can be dynamic or be on a separate constants file.
userFeatures: string[] = [
'feature-1', 'feature-2'
];
accountFeatures: string[] = [
'other-client-features'
];

constructor() { }
constructor() {
// Spin up things.
this._initSdk();
}

initSdk(): void {
// Running the SDK in 'off-the-grid' Mode since authorizationKey : 'localhost'
// To bind a non 'off-the-grid' client, inject the real API Key
this.splitio = SplitFactory({
private _initSdk(): void {
this._factory = SplitFactory({
core: {
authorizationKey: 'localhost',
key: 'customer-key'
authorizationKey: '<your-api-key>',
key: '<main-client-key>'
},
startup: {
readyTimeout: 3
},
// In non-localhost mode, this map is ignored.
features: {
feature_1: 'off',
feature_2: 'on',
feature_3: 'v2'
// to showcase faster reloads.
scheduler: {
featuresRefreshRate: 15,
segmentsRefreshRate: 15,
metricsRefreshRate: 15,
impressionsRefreshRate: 15,
eventsPushRate: 15
}
});

this.splitClient = this.splitio.client();
// Get client
this._splitUserClient = this._factory.client();
// and the one for a different key. This will share resources with the main one but segment data.
this._splitAccountClient = this._factory.client('<second-client-key>');

// verify if sdk is initialized
this.verifyReady();
// Handle events
this._setupSubscriptions(this._splitUserClient);
this._setupSubscriptions(this._splitAccountClient);
}

private verifyReady(): void {
const isReadyEvent = fromEvent(this.splitClient, this.splitClient.Event.SDK_READY);
private _setupSubscriptions(client): void {
// Overwriting the reference to keep the latest one, since we're using it
this._readyPromise = new Promise(resolve => {

const subscription = isReadyEvent.subscribe({
next() {
this.isReady = true;
console.log('Sdk ready: ', this.isReady);
},
error(err) {
console.log('Sdk error: ', err);
this.isReady = false;
}
// Once the SDK is ready, we will resolve with true to render the page.
client.on(client.Event.SDK_READY, () => {

// If the SDK timed out on the limit we have, but we're ready now, get our treatments.
// and also replace the promise we return from the other method.
if (this._isSdkReady === false) {
this._readyPromise = Promise.resolve(true);
this._getTreatments();
}

this._isSdkReady = true;
resolve(true)
});

// We won't throw an error here, we'll just return the false for the AppResolver to know it should not render the view.
// A real use case would be to handle that as a control event.
//
// There's an advanced use case where you'll react to timeouts but once the SDK gets ready you'll use it.
// Just showcasing a way you can handle that, but if you have a specific use case we can have more discussions.
client.on(client.Event.SDK_READY_TIMED_OUT, () => {
this._isSdkReady = false;
resolve(false);
});
}).then((isReady: boolean) => {

// Once the SDK has resolved somehow, update the treatments list.
this._treatmentsSubject.next(this._getTreatments());

// And we'll also subscribe to updates and refresh our flags on that event, using the observable.
client.on(client.Event.SDK_UPDATE, () => {
this._treatmentsSubject.next(this._getTreatments());
});

return isReady;
});
}

getTreatments(): void {
this.treatments = this.splitClient.getTreatments(this.features);
/**
* Return the treatments mixing up all flags. You could have two different functions.
*/
private _getTreatments(): SplitIO.Treatments {
const newUserFlags = this._splitUserClient.getTreatments(this.userFeatures);
const newAccountFlags = this._splitAccountClient.getTreatments(this.accountFeatures);

const newFlags = {
...newUserFlags,
...newAccountFlags
};

this.treatments = newFlags;
this._treatmentsSubject.next(newFlags);

return newFlags;
}

getReadyPromise(): Promise<boolean> {
// Just return the promise, in case we need to ask for readiness later.
return this._readyPromise;
}
}