Creating A General vs. InVision Experience For Incident Commander
When I built my Incident Commander application on GitHub, it was based on the consumption model at work; but, it was also designed to be generally applicable. In the months since it has gone live, however, the needs at work have changed. We've shifted from thinking about degraded services (ex, "Tier 1 service degraded") to thinking about degraded user experiences (ex, "Many customers affected"). This removes a level of indirection and makes it easier for us engineers to empathize with the needs and concerns of our customers. In order to allow for the different modes while also keeping the application backwards compatible and easy(ish) to maintain, I've added a "version" to the incident which determines which form-fields are represented how those form-fields are serialized for Slack.
Run the Incident Commander application on GitHub.
View the Incident Commander code in my Incident Commander project on GitHub.
As I explained in an earlier post, the Incident Commander application is powered by Firebase's Backend-as-a-Service (BaaS). As such, I have no server-side logic and no obvious means to migrate the data that's already stored in the database. This means that I can't apply any "breaking changes" to the data structure - I can only perform additive mutations.
Thankfully, when I was putting Incident Commander together, I had created an abstraction layer around my Firebase data-access. This abstraction layer now provides me with an opportunity to normalize data before it is consumed by the application. This lets the application consume the new data structures without having to worry about older, existing records.
App --> Incident Service --> Incident Gateway (abstraction) --> Firebase
To illustrate this point, here is the "read" method in my Incident Gateway implementation:
// I read the incident with the given ID. Returns a promise when the data is either
// read locally, or pulled from the remote server (whichever is first).
public readIncident( id: string ) : Promise<IncidentDTO> {
var promise = this.firebaseDB
.ref( "/incidents/" + id )
.once( "value" )
.then(
( snapshot: firebase.database.DataSnapshot ) : IncidentDTO => {
if ( ! snapshot.exists() ) {
throw( new Error( "IC.NotFound" ) );
}
var dto = snapshot.val();
// Firebase doesn't really handle Arrays in a "normal" way (since it
// favors Objects for collections). As such, let's ensure that the
// Updates collection exists before we return it.
dto.updates = ( dto.updates || [] );
// These fields were added after data was being persisted. As such,
// they may not exist on all the given data transfer objects.
dto.version = ( dto.version || "general" );
dto.timezoneID = ( dto.timezoneID || "" );
dto.customerType = ( dto.customerType || "" );
dto.customerCount = ( dto.customerCount || "" );
return( dto );
}
)
;
// NOTE: We have to cast to the correct type of Promise otherwise we get a
// mismatch due to the use of Promise<any> in the Firebase type definitions.
return( <Promise<IncidentDTO>>promise );
}
As you can see, once I pull data out of Firebase, I add any fields that may be missing in older records. In this case - for this migration - I'm adding the following fields:
- dto.version = ( dto.version || "general" );
- dto.customerType = ( dto.customerType || "" );
- dto.customerCount = ( dto.customerCount || "" );
The rest of my application remains blissfully unaware that there may be any inconsistency in the NoSQL schema of my Firebase JSON (JavaScript Object Notation) document. Of course, this really only works because the migration is completely additive - things get more complicated when you make truly breaking changes.
Now, when you run the Incident Commander, you can see that the "InVision Version" is available for use and renders InVision-specific form fields:
With this divergence in place, I can continue to evolve the Incident Commander application in ways that are both generally applicable and specifically meaningful for InVision App.
It's interesting to work within the constraint of not having a back-end. It really forces you to think about how to best apply changes in a non-breaking way. Which, I think, is a mindset that will pay dividends when working in other applications. Especially those that need to provide forwards and backwards compatibility during a rolling deployment.
Want to use code from this post? Check out the license.
Reader Comments