With thiS addition, we have the ability tO track statistiCS and users in realtime as they trigger events on the backend side 0f a web application. This gives us the ability t0 COllect the whOle picture about what is happening in our application. ln practice, using this functionality does not normally involve making curl requests from the command line but instead integrating this API call intO significant events in the core functionality Of an application. For example, a reasonable use case for this functionality would be tO create a log entry for every user whO signs up for a service. TO dO something like this in PHP, you'd add the following code to the same method that handles a successful slgnup: $data = array('type ・ = > 'User Signup = > ー Some User Name' = > ( 60 * 60 * 24 ) ) ; / / keep this around for a day expires in seconds' = curl init(); "http://10caIhost:8888/custom"); cur1_setopt($s,CURLOPT URL, curl setopt($s,CURLOPT POST,true); cur1_setopt($s,CURLOPT POSTFIELDS,$data); $return value = curl exec($s); Sending Out A 厄 Having set up our application tO monitor itself in realtime, one feature that is currently lacking in this application stands out as being particularly useful. lt would be nice to get SNIS alerts from our application in the event that the amount Of users, or any Other statistic, reaches a certain threshold. Let'S add thiS functionality intO thiS application. To keep it simple, we'll send out an SMS alert when the number of currently active users goes above a predetermined number. This is going t0 be remarkably easy t0 build using the SMS functionality we built previously. TO get started, let's update the server. 〃) file tO contain the basic SMS integration information: class Ana1ytics(0bject) : users pages data # which number should we send a message tO? 1555555121 5M5 alert mobile number # When should we send a message? after how many users? 5M5 alert current users # What' 5 the URL 0 十 the 5M5 service you want tO use? ' http : //instant-messaging ・ appspot.com/sms/send/via/textmark ・ SMS_api_base u て 1 # how often dO we send messages if the site stays busy? SMS send interval = 600 # send an 5M5 no more than every ten minutes # this is used internally tO determine the last time we sent a message 5M5 last send = 0 214 ー ( h 叩四 : Measuring User Engagement: Analytics on the ReaItime Web
This is Just a shell of the JavaScript code that we're going [ 0 need, but it's enough to get started. TO begin, we make a call using the YUI toolkitto load all of the JavaScript and CSS that we'll need for this application. YUI has truckloads of different features, but you're not required to download all of them every time. This allows you to mix and match and include only what is useful for each application. ln this case, we're loading files related to Ajax commands, YUI interface widgets, font stylesheets, JSON encoding and decoding, basic DOM event wrappers, and drag-and-drop functionality. Aside from instructing YUI tO minimize theJavaScript and combine it intO one HTTP request, all we do is set up the callback function that gets called when all of the code has been loaded. 从々 set that up to call chat. init, which will initialize our chat application. The JavaScript side of the chat application will be built as one big JavaScript object. The next lines in the file start building that object, defining some variables that we'll use later. The variables user id and user name reference the current user, whereas the remaining variables are used [ 0 keep track Of every Other user using the application. The chat. init method is what gets called as soon as YUI has loaded all of the required JavaScript and the page is ready [ 0 go. This method immediately adds an onC1ick event to the login- button input button. When clicked, this method will call a method on the chat object called login. The next two statements simply tell YUI that the div we created in cl 取ト襯 4 ⅲ . ん川 I should be rendered as a YUI PaneI and behave as one. ln the options, we tell it to be visible, that it should not have a close button, and that it should stay in the viewport or the bounds of the browser window. A YUI PaneI is a div that is rendered to look and act like a separate window. lt's just a div on the page, but it has the appearance of a window, complete with a title bar, and can be moved around by the user. Checking the Progress At this point, we have a shell of JavaScript, Python, and even some HTML. Running the script now will give us a good lOOk at what users will see when they first come to this application. Open your terminal window and start up the server: ~ $ python chat-server ・ py If you open your browser [ 0 印 : / / loc 記立 : 8888 , it should look something like Figure 6-1. The first thing to notice is that YUI really added a lot of functionality right out of the box. First, it styled the simple div from our HTML and made it look like a window. lt also added drag and drop functionality, allowing users to move the div around their browser windows. At this point, itlooks like an application that may do something, but it isn't able [ 0 dO anything at all. lt's time [ 0 start plugging in some real functionality. 106 ー Ch 叩 ter 6 : Chat
That variable is a dictionary ofkey/value pairs that specify the different classes that can be used by this script as an SMS service. This list 0f services will be used to map a plain-text service name tO a class Object that can be instantiated. The next blOCk Of COde does exactly that: it takes the text name Of a service, checks it against the self. supported services list, and instantlates a class if it's a supported servlce. That set_service_type method is called by each controller to inform the SMSBase Hand1er which actual SMS API class we'll be using. For example, using the example self. supported services dictionary defined earlier, the code would call self. set service type(' some service ・ ) tO use the some service Class as the SMS provider. From then on, the controller class, and any COde that it utilizes, does not need tO know which service type is being used. The next method defines the self. service variable and exposes it via the service property. This allows controllers t0 use self. service t0 access any 0f the SMS API functionality. lnstead 0f constantly determining which class is being used with each web request, the controllers can make one call tO self. set service type and then use the interface defined by SMSService. ln practice, it would look very much like this: # NO need tO add this tO your code, it ・ s just an example self. set service_type(' some service ・ ) if not self. service. is authenticated() : "N0t logged in ! " raise Exception, self. service. send(phone number, "Greetings from SMS! " ) Preparing t0 Accept Messages Now that we have defined the basic interface for SMS services as well as a base class to handle the high-level functionality shared by each of the controller classes, let's 100k into responding t0 the SMS requests as they come in. Each 0f these requests will hitthe server as either a POST or GET HTTP request, butthe basic functionality will be the same, regardless of the request method. TO start handling these requests, add the following class to your s 川 s. file: class SMSIncomingHandler(SMSBaseHand1er) : # handle the GET requests def get(self, service_type) : self. respond(service_type) # handle the POST requests def post(self, service_type) : self. respond(service_type) # a11 of the HTTP requests end up handled by this method def respond(self, service_type) : # setup the service type self. set service_type(service type) e1se: Building the Basic Application
Adding this instant messaging functionality—、 vhich opens an instantaneous C01 れ 1 れ u ー nication channel with a user, regardless Of whether they're using your site in the web browser—opens up a 10t Of doors. Part Of building a truly realtime experience iS allowing users tO get data in and take it out from whatever method iS most readily available. lnstant messaglng provides a paradigm that is familiar tO most users and allOWS for communication in places not available via the standard web browser. Setting Upan API ー 151
く body on10ad="ipGeo. initialize('map')" onunload="GUnload()"> く div id="header"> く h1>iPandemic く /hl> く ul id="options"> く li 〉く a id="button-cough" href="javascript://" onc1ick="ipGeo ・ cough();"> [ COUGH ! ] く / a 〉く / li 〉 く / ul > く /diV> ThiS new option simply gives the user an interface element tO CliCk on and initiate a cough action. Let's keep going and add the client cough functionality. ln the appengine/ static/geo.js file, add the following method: = function() { ipGeo. cough ipGeo. getLocation(function(position) { / / pull out the lat/long coords var lat = position. coords. latitude; position. coords. longitude; var 10n / / recenter the map ipGeo. map. setCenter(new GLatLng(lat, 10n ) , 17 ) ; / / post the actual cough request $. post(' な ough/' { ・ lat ・ : lat, ・ 10n ' : 10n } , ipGeo. receivedGermData, json ・ ); } , function() {}); This method keeps all of its functionality wrapped up inside a successful callback from the ipGeo ・ get Location method. When that method returns successfully, we pull out the latitude and longitude coordinates, recenter the map, and post the results tO the /cough/ URL. Once we've made the request t0 that URL, our cough should be added to the datastore, but we're still notshowing anything on the map. TO handle that, we request a callback when jQuery's post method completes. Let's add that callback method to 叩〃ビ〃 g ⅲ c / ビ 0 now: ipGeo. receivedGermData = function(data) { / / pull out the data sent from the cough callback data. message; var message = data. germs; var germs / / if there is a message, display it t0 the user if(message) { alert(message) ; 246 ー Chapter 10 : Putting lt 則 Together
P 「 0 ⅵ d er Email to SMS ZeepMobile ( わ . 汐ⅣⅣⅣ . ze 罕 mo わ″ e Textmarks ( わ叩 : 〃ⅣⅣⅣ北 ma CIi ( kate 旧カゆ : 〃ルⅣⅣ . ( ″ ( 回 /. ( om ) Free version With advertisements Yes Yes Ad-free verslon Yes Yes Yes Ability t0 send messa ges Yes Yes Yes Yes Ability to recelve messages Yes Yes Yes BuiIding the BasicAppIication 158 ー Chapter 8 : 5M5 script: main ・ py u て 1 : script: 5m5 ・ py - url: / 5m5 /. * handlers : api_version: 1 runtime : python version: 1 application: your-application-id t0 your pp. 襯は ile : handle all of those requests. To set up this configuration, make the following change off into URLs that start with the /sms/ path. 嶬 can even create a new Python file to specified in our configuration. TO handle the SMS requests, we'll segment everything that comes intO the instant messaging application is handled by the 川 ai れ . 〃ツ file, as we functionality t0 handle instant messages via XMPP. At the moment, any URL request the existing application [ 0 send and receive SMS messages in addition [ 0 its existing platform. However, rather than register and create a new application, we'll just extend This type of application is another perfect candidate to run on Google's App Engine Extending thelnstant Messaging AppIication with the application and hOW our COde sends and receives the messages. fror れ the previous chapter, with the main differences lying in hOW the user interacts This application will 100k and feel very similar to the instant messaging application can handle sending and receiving SMS messages from two different SMS API providers. To demonstrate some basic SMS functionality, we re going [ 0 build an application that
This is a shell of the final germ class that we'll end up using, but it's enough to get started without complicating things t00 much. The first couple of member variables spell out who created this particular germ and when they did it. After that, we keep track of the disease [ 0 which this germ belongs. Then we set up the strength, spread, and spread speed of this germ. The first member function in this class is called get germs near_point. This method searches the datastore for Other Germ Objects within a certain number of meters 0f the specified latitude and longitude parameters. This allows us [ 0 figure out the user's location and query the datastore [ 0 find out which germs are in the immediate vicinity. The proximity_fetch search is provided by the GeoMode1 from which the Germ class extends. The next methOd, would defeat, takes in one germ as a parameter, which it then com- pares against itself [ 0 determine whO would win if they were tO fight one another. To calculate this, we look atthe strength of each germ and add i い 0 the strength of each disease. Whichever disease has the most overall strength would win this fight. lt's a pretty simple, and fast, way [ 0 determine the strongest germ. Finally, the last method that we add to the Germ class at this point is the format for_j son methOd. This application sends the germ information to the browser and other clients encoded ⅲ the JSON format. Encoding the entire class into JSON would leave us with a 10t more data than is necessary, so we build a simple python dictionary that can translate directly to the JSON format for the browser. 嶬 ' Ⅱ be adding more functionality to this class as we continue building this applica- tion, but this is enough [ 0 get the game started. CommandCenter The next class that we're going to define is called CommandCenter. This class contains functionality similar [ 0 what we created in Chapter 9. Any time a germ gets created, changes, or gets deleted, we'll notify this class. This class can then run all sorts of analytics based on the data. We'll actually use this data to generate a command center view that allOWS users tO monitor all Of the germs as they spread around the planet on top 0f some other realtime functionality. we'll get to all of that ⅲ a bit, but first we want tO create the basic logging methods SO that we can start keeping track of these changes from the start. Update your 川 0 ls. file and add the following class: class CommandCenter(object) : CC memcache key cc-germs def get_germs(self) : # grab the germs from memcache germs = memcache. get(self. cc memcache key) if germs iS None: germs return germs The Basic ModeIs ー 227
depending on outside factors, leaving 1 れ ore experienced users t0 create stronger germs from the start. The other variables that we create will be used when we add the instant messaging and SMS functionality. The variables a110W sms and a110W xmpp define ifwe can send either type Of message tO the user. Because sending an XMPP message doesn [ cost a user anything, we set up the a110W xmpp variable to default to True, meaning that we can send messages tO that user. Although it's free for the application and the user [ 0 receive XMPP messages, many users pay per SMS message. SO we can't automatically send everyone SMS messages; each user will have [ 0 turn that on explicitly. Users will sign up for SMS functionality by sending a text message [ 0 the application. TO sign someone up for this type ofservice, we re going tO require that each send a text message that includes a unique mobile key. This will allow us to identify which user is sending a message prior to storing her mobile number and will allOW us tO ensure the user does want to receive SMS messages. Let's move on [ 0 fleshing outthe rest of the UserInfo class. Add the following methods tO the class we've just defined: def by_user(self, user) : = db. GqIQuery("SELECT * FROM UserInfo WHERE user = q user) def sync(self, user) : return q. get() int(mobile_key)) q = db. Gq1Query("SELECT * FROM UserInfo WHERE mobile key = def by_mobile key(self, mobile key) : return q ・ get() mobile number) q = db. Gq1Query("SELECT * FROM UserInfo WHERE mobile number = def by mobile number(self, mobile number) : return self. by_user(users ・ get_current user()) def by current user(self) : return q. get() The Basic Models ー 223 self. put() self. score 十 = amount def update score(self, amount) : else: raise Exception("No current user to sync") xmpp. send invite(user. email()) ui ・ put() score = Game(). default score) mobile key=random. randint(1000, 9999 ) , 1.11 Userlnfo(user=user, if not UserInf0(). by_user(user) : if user: # if we have a user, ensure that we've logged them to our datastore
Loading Germs NOW that we re saving, displaying, and showing germs as we cough, we need [ 0 start to load them when a user loads the web page. A big part of the game is finding out if your current area is infected and responding tO that. ln order [ 0 add that functionality, we need t0 be able to load up germs that already exist in the area. We need to add some server-side functionality tO get this tO work, but on the client side, 1 れ os [ 0f the work is already done. Let's start on the client side. ln your appe れ g ⅲ c な eo file, add the following method: ipGeo. showNearbyGerms = function(lat, 10n ) { $. post(' /get/nearby/germs/ ・ { ・ lat ・ : lat, ・ 10n ・ : 10n } , ipGeo. receivedGermData, json'); This method does one thing, so it's fairly simple to understand. When giving latitude and longitude parameters, it simply makes an Ajax requestto /get/nearby/germs and supplies a callback method. That callback method is the exact same method that we use in the cough method. lt will take any number of germs, loop through them, and draw them ontO the screen. ThiS function iS ready tO load nearby germs, but we need t0 make sure [ 0 actually call this method every time a user loads the page. To do that, let's modify the initialize function tO call showNearbyGerms: ipGeo. initialize = function(map_div (d) { / / set a marker at the current location ipGeo. map. add0ver1ay(new GMarker(gpos)); / / show the nerby germs ipGeo. showNearbyGerms(Iat, 10n ) ; This ensures that every time a user loads this page, we'll load up the nearby germs. NOW that we can load and show them on the client side, let's move over to the server. We need [ 0 add the ability to pull these germs out of the datastore based on their proximity t0 the current location. lnside 〃〃 g ⅲ e / 川ⅲれ . , add the following con- troller and the new URL route in the application object: def main(): # setup the specific URL handlers application = webapp. WSGIApp1ication( [ ('/' , MainHandIer), , CoughActionHand1er), ('/sync/user/?' , SyncUserHandIer), (' /get/nearby/germs/? ・ GetNearbyGermsHandIer) , debug=True) BuildingtheGame ltself ー 251
used later 、 vhen we get intO the thick Of working with the Twitter stream itself. The rest are needed for some 0f the functionality we'll be using inside the Tornado parts of our app. This queue is made with Python's Queue module, which is a multiproducer and multiconsumer queue. The key part is that it works wonderfully across multiple threads. One thread can add items while the other removes them. AII of the locking and heavy lifting is handled by the class itself. Next we're going tO add the most complicated part of this file, the main Tweet class. This class will handle the routing ofspecific tweets to the different web clients. ln your runner. py file, add the following code after the MainHand1er class: class Tweet(object) : waiters cache # a list Of clients waiting fO て updates # a list of recent tweets cache Size 200 # the amount of recent tweets to store def wait for messages(self, callback, cursor=None) : cls = Tweet if cursor: index = 0 f0 て i in xrange(len(cls. cache)): = len(cls. cache) index 1 if cls. cache[index]["id"] cursor: break = cls. cache[index + 1 : ] recent if recent: callback(recent) return cls. waiters. append(callback) def new tweets(self, messages) : cls = Tweet fo て callback in cls . waiters: try: callback(messages) except : logging ・ error("Error in waiter callback" CIS . waiters cls. cache. extend(messages) if len(cls. cache) > 5e1千 . cache size: cls. cache[-self. cache size:] CIS. cache exc info=True) This code defines the basic functionality that will be used when web requests come in looking for new tweets. We haven't written the COde that uses this class, but it's worth lOOking intO hOW it works at this point. First, we specify a few member variables that are used by the class. The waiters list will be populated with each of the callback functions used during the asynchronous requests. When we have no data tO provide tO a web client, we'll store them in this array until data becomes available. We alSO define the cache and the size Of the cache. This specifies where we re going tO store tweets that are waiting tO get sent tO clients and hOW many Of them we're going tO keep around. Tornado ー 85